Overview
When installing software built from source, a common and useful pattern is to support a staged install using DESTDIR.
This allows a project to install files into a temporary directory tree first, instead of writing directly into system locations like /usr/local or /etc.
The basic idea is:
define the intended install layout with variables like PREFIX, BINDIR, LIBDIR, etc.
prepend DESTDIR during installation
let the caller choose whether installation goes to the real root filesystem or a temporary staging root
This makes installs safer, more flexible, and more packaging-friendly.
Why Use DESTDIR
A normal source build often looks like this:
./configure make sudo make install
That only works cleanly if make install does nothing except copy already-built files.
In practice, some projects mistakenly do build work during install. If that happens, running sudo make install can break because:
root may not have the same PATH
language toolchains may not be available
caches and config may resolve under /root
files in the source tree may become root-owned
Supporting DESTDIR lets you run install logic as a normal user first, then elevate only for the final copy into the real filesystem.
Most packaging systems do not install files directly into /usr, /etc, and so on while assembling a package.
Instead, they stage files into a temporary root tree such as:
pkgroot/ usr/local/bin/myapp usr/local/share/myapp/... etc/myapp/config.conf
That staged directory is then turned into a package or copied into a target system.
DESTDIR is the standard convention that makes this possible.
A staged install can be reviewed before touching the real system.
Example:
make install DESTDIR="$PWD/pkgroot" find "$PWD/pkgroot"
This lets you verify:
what files are being installed
where they will go
whether paths are correct
whether anything unexpected is being written
That beats discovering after the fact that your install target quietly scribbled into /etc like an overconfident raccoon.
DESTDIR and PREFIX solve different problems:
PREFIX controls the intended install location
DESTDIR adds a temporary root for staging
Example:
PREFIX = /usr/local BINDIR = $(PREFIX)/bin DESTDIR = /tmp/pkgroot
Final staged file path becomes:
/tmp/pkgroot/usr/local/bin/myapp
Later, that file may be copied into:
/usr/local/bin/myapp
This separation is what makes packaging and system installs behave sanely for once.
DESTDIR Is a Convention, Not a Built-In Make Feature
make itself does not know anything about DESTDIR.
This works:
make install DESTDIR=/tmp/pkgroot
only because the Makefile author chose to write install rules using $(DESTDIR).
So supporting DESTDIR means explicitly designing install rules to prepend it.
The Basic Pattern
A Makefile that supports staged installs typically looks like this:
PREFIX ?= /usr/local BINDIR ?= $(PREFIX)/bin LIBDIR ?= $(PREFIX)/lib DATADIR ?= $(PREFIX)/share/myapp
install: install -d $(DESTDIR)$(BINDIR) install -d $(DESTDIR)$(LIBDIR) install -d $(DESTDIR)$(DATADIR)
install -m 0755 myapp $(DESTDIR)$(BINDIR)/myapp
install -m 0644 libmystuff.so $(DESTDIR)$(LIBDIR)/libmystuff.so
cp -R assets/. $(DESTDIR)$(DATADIR)/
How this behaves
With no DESTDIR:
make install
files go to:
/usr/local/bin/myapp /usr/local/lib/libmystuff.so /usr/local/share/myapp/...
With DESTDIR:
make install DESTDIR="$PWD/pkgroot"
files go to:
$PWD/pkgroot/usr/local/bin/myapp $PWD/pkgroot/usr/local/lib/libmystuff.so $PWD/pkgroot/usr/local/share/myapp/... Recommended Makefile Structure
A clean Makefile should separate:
build steps
install steps
Example:
PREFIX ?= /usr/local BINDIR ?= $(PREFIX)/bin
TARGET := myapp
.PHONY: all build install clean
all: build
build: $(TARGET)
$(TARGET): main.o $(CC) -o $@ main.o
main.o: main.c $(CC) -c main.c
install: $(TARGET) install -d $(DESTDIR)$(BINDIR) install -m 0755 $(TARGET) $(DESTDIR)$(BINDIR)/$(TARGET)
clean: rm -f $(TARGET) main.o
This is good because:
build creates the artifact
install copies the artifact
DESTDIR affects only install location
there is no need for root during compilation
Better Pattern for Multi-Step Projects
If your project has binaries, libraries, configs, and docs, use dedicated path variables.
Example:
PREFIX ?= /usr/local BINDIR ?= $(PREFIX)/bin LIBDIR ?= $(PREFIX)/lib ETCDIR ?= /etc/myapp DOCDIR ?= $(PREFIX)/share/doc/myapp
install: install -d $(DESTDIR)$(BINDIR) install -d $(DESTDIR)$(LIBDIR) install -d $(DESTDIR)$(ETCDIR) install -d $(DESTDIR)$(DOCDIR)
install -m 0755 myapp $(DESTDIR)$(BINDIR)/myapp
install -m 0644 libmyapp.so $(DESTDIR)$(LIBDIR)/libmyapp.so
install -m 0644 myapp.conf $(DESTDIR)$(ETCDIR)/myapp.conf
install -m 0644 README.md $(DESTDIR)$(DOCDIR)/README.md
This makes the install layout explicit and predictable.
Important Rules for DESTDIR
Do this:
install -m 0755 myapp $(DESTDIR)$(BINDIR)/myapp
Do not do this in build rules:
$(CC) -o $(DESTDIR)$(BINDIR)/myapp main.o
DESTDIR is for installation, not compilation.
Do this:
BINDIR ?= $(PREFIX)/bin install -m 0755 myapp $(DESTDIR)$(BINDIR)/myapp
Do not hardcode paths like this:
install -m 0755 myapp /usr/local/bin/myapp
Hardcoded paths make staging impossible and packaging miserable.
Use install -d or mkdir -p before copying files:
install -d $(DESTDIR)$(BINDIR)
This ensures the staged tree is built correctly and repeatably.
This is one of the most important habits.
Bad:
install: cargo build --release install -m 0755 target/release/myapp $(DESTDIR)$(BINDIR)/myapp
Better:
build: cargo build --release
install: install -d $(DESTDIR)$(BINDIR) install -m 0755 target/release/myapp $(DESTDIR)$(BINDIR)/myapp
Best:
install: build install -d $(DESTDIR)$(BINDIR) install -m 0755 target/release/myapp $(DESTDIR)$(BINDIR)/myapp
This keeps toolchains out of privileged install flows and avoids weird environment issues.
Example: Rust Project with DESTDIR PREFIX ?= /usr/local BINDIR ?= $(PREFIX)/bin TARGET ?= target/release/myapp
.PHONY: build install clean
build: cargo build --release
install: build install -d $(DESTDIR)$(BINDIR) install -m 0755 $(TARGET) $(DESTDIR)$(BINDIR)/myapp
clean: cargo clean
Usage:
make build make install DESTDIR="$PWD/pkgroot" sudo rsync -a --dry-run "$PWD/pkgroot"/ / sudo rsync -a "$PWD/pkgroot"/ / Example: C Project with DESTDIR PREFIX ?= /usr/local BINDIR ?= $(PREFIX)/bin
CC ?= cc CFLAGS ?= -O2
TARGET := hello
.PHONY: all install clean
all: $(TARGET)
$(TARGET): hello.o $(CC) $(CFLAGS) -o $@ hello.o
hello.o: hello.c $(CC) $(CFLAGS) -c hello.c
install: $(TARGET) install -d $(DESTDIR)$(BINDIR) install -m 0755 $(TARGET) $(DESTDIR)$(BINDIR)/$(TARGET)
clean: rm -f $(TARGET) hello.o Testing DESTDIR Support
A quick way to verify your Makefile supports staging properly:
make -n install DESTDIR="$PWD/pkgroot"
Inspect the output.
Good output will show paths like:
/tmp/project/pkgroot/usr/local/bin/myapp
Bad output will still show absolute paths like:
/usr/local/bin/myapp
If absolute paths remain, then your Makefile is not honoring DESTDIR.
Common Mistakes Rebuilding during install
If install triggers compilers or language toolchains, you may run into root environment problems.
Hardcoding /usr/local
This blocks staging and makes packaging harder.
Mixing DESTDIR into logical path variables
Keep logical install paths in PREFIX, BINDIR, etc. Apply DESTDIR only at install time.
Forgetting config paths outside PREFIX
Some files belong under /etc, not under /usr/local. That is fine. Just still prepend DESTDIR during installation:
ETCDIR ?= /etc/myapp install -m 0644 myapp.conf $(DESTDIR)$(ETCDIR)/myapp.conf Recommended Workflow
For local system installs:
make make install DESTDIR="$PWD/pkgroot" sudo rsync -a --dry-run "$PWD/pkgroot"/ / sudo rsync -a "$PWD/pkgroot"/ /
For package builds:
make make install DESTDIR="$PKGDIR"
For direct installs where install truly only copies files:
make sudo make install
That last one is fine when the install target is clean. The problem is that many projects are not nearly as clean as their authors imagine.
Summary
A Makefile should support DESTDIR because it allows:
staged installs
safer review of installed files
package building
avoiding root during build and install logic
cleaner separation between build and deployment
The core design rule is:
Build artifacts first. Install them second. Prepend DESTDIR during installation.
That pattern is simple, durable, and worth internalizing.
If you want this as a real README.md file, I can format it into a tighter version with a table of contents and shorter examples.