Compile C89 Code Properly with c89-gcc
The c89-gcc wrapper is what stands between your build and a codebase that only claims ANSI portability. Follow this to compile a C89 program, prove strict mode is actually on, and tell a real source bug from the wrapper simply refusing your flags. Allow about ten minutes.
The route
Jump straight to the step you need, or tick off Done means at the end.
You need a shell, a readable C source file and write access to wherever the compiler puts its output. The examples use GCC 13.3.0, what c89-gcc --version reports on this machine. Debian's package metadata calls the installed gcc package 4:13.2.0-7ubuntu1; trust the compiler's own version output over the package name when recording the toolchain used for a build.
Checkpoint
This guide only compiles and checks code. It does not install a compiler, touch system headers or need sudo.
1. Check which compiler you are actually calling
c89 and c89-gcc are the two names this setup exposes, both pointing at the same GCC-backed wrapper. Confirm the path and version before you trust anything it produces:
$ command -v c89-gcc
/usr/bin/c89-gcc
$ c89-gcc --version
gcc (Ubuntu 13.3.0-6ubuntu2~24.04.1) 13.3.0
The real version banner has more lines than this. What matters is that the command was found and it is the GCC release you expect. If command -v turns up a project-local script or a different compiler, stop and check your PATH before comparing results with this guide.
2. Compile a small C89 program
Point it at a source file, shown here as /path/to/hello.c. Give the output a new name so you are not overwriting anything:
$ c89-gcc /path/to/hello.c -o /path/to/hello-c89
No diagnostic means GCC succeeded and the executable is ready. Behind the scenes the wrapper adds -std=c89 before handing your arguments to gcc. That selects the C89 dialect, defines __STRICT_ANSI__, and switches off GNU extensions like inline, typeof and the $ character in identifiers.
Check the file type and the exit status separately:
$ file /path/to/hello-c89
/path/to/hello-c89: ELF 64-bit LSB pie executable, x86-64, ...
$ /path/to/hello-c89
$ printf '%s\n' "$?"
0
Your file output may read differently depending on architecture and linker defaults. A clean compile only proves the code builds, not that it is correct, so run the binary and check its result too.
3. Prove the source actually sees strict mode
If a build genuinely depends on the dialect rather than assuming it, ask the preprocessor directly:
$ printf '' | c89-gcc -dM -E - | grep '__STRICT_ANSI__'
#define __STRICT_ANSI__ 1
The macro only exists after preprocessing, so grepping raw -E output for its name tells you nothing. A source-level check works too, something like printf("%s\n", __STRICT_ANSI__ ? "strict" : "not-strict");, which prints:
strict
Checkpoint
-E stops after preprocessing and writes plain C source to the named file. It builds nothing. Keep that throwaway output in /tmp or wherever your generated files live.
4. Build an object file for a bigger project
Multiple translation units compile separately with -c, no linking:
$ c89-gcc -c /path/to/parser.c -o /path/to/parser.o
$ file /path/to/parser.o
/path/to/parser.o: ELF 64-bit LSB relocatable, x86-64, version 1 (SYSV), ...
An object file will not run. Link the pieces later, still through the wrapper, so the final compile settings stay visible in the build command:
$ c89-gcc /path/to/parser.o /path/to/main.o -o /path/to/parser-tool
Want a source check with no object file and no executable? Use -fsyntax-only:
$ c89-gcc -fsyntax-only /path/to/parser.c
$ printf '%s\n' "$?"
0
5. Make diagnostics stricter for old code
Selecting C89 is not the same as making GCC reject every extension as an error. Add one of these when reviewing legacy source:
-pedanticwarns about constructs outside the chosen ISO dialect.-pedantic-errorsfails the build on the same diagnostics.
$ c89-gcc -pedantic -Wall /path/to/hello.c -o /path/to/hello-c89
$ c89-gcc -pedantic-errors -Wall /path/to/hello.c -o /path/to/hello-c89
Both pass straight through to GCC. -Wall turns on a useful warning group, but it is not a promise every defect gets caught. Keep warnings visible in CI and decide separately whether your project treats them as failures.
Do not let a later standard sneak in. The wrapper treats -ansi, -std=c89 and -std=iso9899:1990 as the same request, and rejects any other -std=... before GCC even sees it:
$ c89-gcc -std=c99 /path/to/hello.c -o /path/to/hello-c89
c89-gcc called with non ANSI/ISO C option -std=c99
Need C99 or newer on purpose? Call gcc directly. That is a different build decision, not a fix for a failed C89 build.
6. Diagnose failures without wrecking a working build
Compiler errors land on standard error. The local manual notes this wrapper can prefix them with cc:, though the exact prefix depends on the installed wrapper and diagnostic path. Compile to a new name while you investigate, so the previous build survives:
$ c89-gcc -pedantic-errors /path/to/hello.c -o /path/to/hello-c89.new
$ status=$?
$ printf 'compiler status: %s\n' "$status"
compiler status: 0
If the command fails, read the diagnostic and fix the source or its include paths, and do not touch the working executable until the new command succeeds and you have actually run it. Shell redirection or a reused -o name will happily overwrite a good build, so never point an experiment at a known-good output name.
A missing input is a different problem from a C error. Check it without reaching for elevated access:
$ test -r /path/to/hello.c && echo readable
$ ls -l /path/to/hello.c
Only use elevated access if the source or destination is deliberately locked down. Changing ownership or permissions just to make a build pass hides a real deployment problem. Copy the source into a build directory you control and fix its ownership rules instead.
Done means
- Confirmed the wrapper.
command -v c89-gccfound the intended binary and you recorded its version. - Compiled clean. The source built in C89 mode and the program returned the expected exit status.
- Verified strict mode. A preprocessing or source-level check confirmed
__STRICT_ANSI__where the build needed that guarantee. - Kept steps separate. Object-only and syntax-only checks never got mixed up with linking.
- Chose stricter diagnostics deliberately.
-pedanticor-pedantic-errorsonly when you meant to add them. - Protected the working build. Failed experiments used a new output name, leaving any known-good build recoverable.