Only a makefile (which normally does not have an "extension" (that's a windows thing, with Linux it's just a dot in a filename)) and your c source code is not enough.
As already mentioned before, you also need a linker script (this tells the compiler how much memory your uC has, and addresses of different sections. And you need a file with startup code. That is code which initializes (static) variables, and calls your main() when it's finished with initialization.
a makefile is not strictly necessary, but remembering how to combine all the parts by the command line would be atrocious. But you could for example write a python script to do so, use scons, cmake or other "build systems". All those build systems are quite involved and complicated, and add another layer of confusion to the learning curve.
I've mostly decided to stick with makefiles even though I find it's syntax atrocious.
Recently I used a bit of ai to write a makefile for me (for stm32) and it did a quite decent job. It made a quite readable makefile, that has just enough to make it a bit universal (such as declaring CC variables and such). These AI things have grokked enough data to fill in more details of the whole compile process then I could do from memory. (I used duckduck.com, which uses different AI's depending on ...)
When using a C compiler, also use the make utility that fits with that compiler. Using a different make can (apparently) lead to problems. I never bumped into that myself (yet). I only use variants of GCC, both for PC software and uC's.