Awk
Since Awk reads the file sequentially, from the first to the last line, without external help (e.g. Tac) it can only figure whether a block of empty lines is at the end of the file when it actually reaches the end of the file.
What you can do is keep a variable with the empty lines (i.e., only newline characters, the default record separator RS) and print those empty lines whenever you reach a non-empty line:
awk '/^$/{n=n RS}; /./{printf "%s",n; n=""; print}' file
I don't understand why there is a difference between print n and printf n.
print appends the output record separator (ORS, by default a newline) to the expression to be printed. Thus you would get an extra newline if you tried it. You could also write it with a single output statement as in
awk '/^$/{n=n RS}; /./{printf "%s%s%s",n,$0,RS; n=""}' file
To print the output (just as Awk did), choose either of
printf '%s\n' 'a' '' '.' '?.?+1,$d' ',p' 'Q' | ed -s file
printf '%s\n' 'a' '' '.' '?.?+1,$d' '%p' 'q!' | ex -s file
To directly apply the changes to the file, choose either of
printf '%s\n' 'a' '' '.' '?.?+1,$d' 'w' 'q' | ed -s file
printf '%s\n' 'a' '' '.' '?.?+1,$d' 'x' | ex -s file
To understand what's going on.
Shells strip trailing newline characters in command substitution.
printf '%s\n' "$(cat file)"
Mind that some shells will not handle large files and error with "argument list too long".
Inspired by this answer.