$ make build util.c frame.c shapes.c tetris.c CC -Wall util.c frame.c shapes.c tetris.c /usr/bin/ld: /tmp/ccMMTOCD.o:(.bss+0x0): multiple definition of `tv'; /tmp/ccTozTNs.o:(.bss+0x0): first defined here /usr/bin/ld: /tmp/ccMMTOCD.o:(.bss+0x80): multiple definition of `frame'; /tmp/ccTozTNs.o:(.bss+0x80): first defined here collect2: error: ld returned 1 exit status make: *** [Makefile:16: tty-tetris] Error 1 That was the first thing the program ever said to me. No binary, no game, just the linker refusing to do its job. Twenty-seven errors, and the code had not run a single line yet.
This week dev.to is full of people waking up dead code, so I picked a patient of my own: a Tetris clone called tty-tetris, sitting untouched on GitHub since 15 January 2009. Ten commits in its whole life, the first one on Christmas Day 2008. Seventeen years and ten months of silence. I wanted to know what happens when you take code that old and feed it to a compiler from 2026 without changing a thing.
The patient
The repo is xorg62/tty-tetris, described by its author as "a simple tetris game wrote in C (without ncurses)". I picked it for three reasons, and all three are boring on purpose. It is small: 786 lines of C and headers across four .c files, plus a 37-line Makefile. It has zero outside dependencies, no ncurses, no X11, just termios and ANSI escape codes, so nothing else had to be installed or hunted down. And it was genuinely abandoned, not just quiet for a year: the last commit message reads "Modify nextbox", and then nothing, ever.
The Makefile is seven lines of real content. It compiles four files with cc -Wall and calls the result tty-tetris. My machine runs GCC 13.3.0 and GNU Make 4.3, both stock Ubuntu. Nothing exotic. That was the whole setup, and it still fell over.
What 27 errors actually means
The error count sounds dramatic, but it is really one mistake repeated. Nine global variables live in the shared header tetris.h:
const int shapes[7][4][4][2]; struct itimerval tv; struct termios back_attr; shape_t current; int frame[FRAMEH + 1][FRAMEW + 1]; int frame_nextbox[FRAMEH_NB][FRAMEW_NB]; int score; int lines; Bool running; Every one of the four .c files includes that header. So the compiler builds four copies of each variable, one per file, and hands all four to the linker. The linker sees score defined four times and says no. Nine variables times three extra copies each gives you the 27 "multiple definition" lines in my build log, plus one fatal ld returned 1 exit status at the end.
Here is the part that makes this a story about time rather than a bug. This code was never correct by the letter of the C standard. It was tolerated. GCC before version 10 defaulted to -fcommon, a mode that quietly merges duplicate tentative definitions into one, the way a tired teacher might let a repeated answer slide. GCC 10 flipped the default to -fno-common. The rule did not change. The enforcement did. Code written in 2008 compiled fine for a decade on a compiler that was being polite, and then the politeness ended.
That is the real shape of "legacy code" in my experience now. The bits do not decay. Nothing in those 786 lines changed since 2009. The ground moved under them instead: stricter defaults, removed functions, headers that moved. The program is frozen. The world is not.
The fix: one line
I had two ways to fix this. The proper one is to rewrite the header so the variables are declared extern there and defined exactly once in a single .c file. That is the right repair, and it touches nine lines across two files. The minimal one is to tell the compiler to be polite again. I took the minimal one, because the point of the exercise was resurrection, not renovation:
-CFLAGS = -Wall +CFLAGS = -Wall -fcommon One flag in the Makefile. No source file touched. I rebuilt, and the whole tone of the output changed:
build util.c frame.c shapes.c tetris.c CC -Wall -fcommon util.c frame.c shapes.c tetris.c Make exited 0. Errors: 0. The binary landed at 21,808 bytes, an ELF 64-bit executable, built from code older than some of the laptops it now runs on.
Eight warnings survived the fix, and I left them alone on purpose. All eight are the same warning, twice per source file: variably modified 'frame' at file scope, pointing at tetris.h:112. The frame dimensions are defined as (int)(9*2.3) and (int)(10*2.3), casts of floating-point math, which C does not accept as a true constant for a file-scope array size. The compiler grumbles and carries on. It is a warning, not an error, and fixing it would mean editing the original author's arithmetic. I wanted the game, not a rewrite, so the warnings stay as a small plaque on the wall: this code is old, it knows it, and it still works.
The run
A Tetris game needs a terminal, so I opened one, played a few moves, hit rotate once, and then q to quit. It drew its frame, showed Score: and Lines: 0, redrew as the pieces fell, and when I pressed q it said goodbye in the most 2009 way possible:
Bye! Your score: 0 Exit code 0. Three seconds of play, a game I lost on purpose. The score was 0 because I quit before clearing a line, not because the game was broken. It played. The pieces fell, the frame held, the quit key worked exactly as config.h promised back in 2008.
So the final tally: 27 errors, 8 warnings, 1 changed line, 0 changed source files, 1 working game, and a score of 0 that I earned honestly.
What I actually learned
The tempting lesson is "old code is fragile", but the run says the opposite. The code was fine. It did exactly what it did in 2009, the moment I gave it a compiler willing to accept its one bad habit. What changed was the contract between the code and the tools, and that contract changes silently, one default flag at a time, while nobody is watching the repo.
That matters more than it sounds, because most abandoned code dies this way: not from a bug anyone found, but from a toolchain that stopped forgiving it. The fix here cost one flag and ten minutes, most of which was reading the linker output properly instead of panicking at the count. The 27 errors were one error wearing 27 coats.
If you have a dead program of your own sitting in a repo somewhere, try this before you rewrite it. Clone it, run the build, and read the first error all the way to the end. Count how many of the errors are the same one. You might find the patient was never dead at all. It was just waiting for someone to argue with the linker on its behalf.
P.S. Everything I write stays free — that part doesn't change. But if this one was worth something to you, you can buy me a coffee ☕ (one-time or monthly, both work). It covers the API credits and hosting behind these experiments — and honestly, it's also the nudge that gets the next one started on the evenings I'd otherwise skip it. — Salam


