> cat intro.txt
Hello, I'm Piero.

I'm a 23 year old software developer mostly experienced in backend who has recently taken on frontend development as well.

This page is mostly for showcasing some projects and posting articles about things I'm deeply interested on.

For some information about me, check out this page!
Recent articles

Proper C Makefiles

2025-08-02
60

Often times comes the need to use a Makefile for compiling our C/C++ library or application and we may end up making some simple version of it which may work for a while. However, with rapidly growing projects and the need to add different features to our compilation scripts it is better to follow some rules at the time of first making our Makefiles.

To achieve this we must follow some good practices that will allow for a short and concise script that can be read and debugged easily, as well as later make full use of some features GNU Make provides.

Build directory

One common occurance is not making a build directory and directly compiling our targets into the root of our project or maybe making the directory without proper structuring, where all blobs and targets are mangled together in a single directory, which may make things hard to find and/or clean. So to best avoid issues like that, we must make a cache/ and build/ directories to separate well our targets from our pre-compiled objects.

Example:

Let's say our project has the following file structure:

.
├── src/
│   ├── services/
│   │   ├── client.c
│   │   ├── client.h
│   │   ├── server.c
│   │   ├── server.h
│   │   ├── mod.c
│   │   └── mod.h
│   ├── utils/
│   │   ├── log.c
│   │   ├── log.h
│   │   ├── mod.c
│   │   └── mod.h
│   ├── lib.c
│   ├── lib.h
│   └── main.c
└── Makefile

Where:

  • lib.c compiles to our library
  • main.c compiles to our executable
  • *.c compile to our object files
  • *.h are copied to our include/

Target:

.
├── src/
│   └── ...
├── build/
│   ├── bin/
│   │   └── project
│   ├── lib/
│   │   └── libproject.so
│   └── include/
│       └── project/
│           ├── services/
│           │   ├── client.h
│           │   ├── server.h
│           │   └── mod.h
│           ├── utils/
│           │   ├── log.h
│           │   └── mod.h
│           └── lib.h
├── cache/
│   ├── services/
│   │   ├── client.o
│   │   ├── server.o
│   │   └── mod.o
│   ├── utils/
│   │   ├── log.o
│   │   └── mod.o
│   └── lib.o
└── Makefile

Apart from our build/ directory we will need a cache/ to store all our object files and keep our build directory clean from all artifacts of compilation.

To achieve this we must make the following definitions:

Sources and targets

# We can define something like "TARGET" as to not have
# to copy the name of our library/application in every
# recipe
TARGET = project

# Source
SRC = $(shell find src/ -name '*.c' ! -name 'main.c')
INC = $(shell find src/ -name '*.h')

# Cache
OBJ = $(SRC:src/%.c=cache/%.o)

# Build
INCLUDE = $(INC:src/%.h=build/include/$(TARGET)/%.h)
LIB = build/lib/lib$(TARGET).so
BIN = build/bin/$(TARGET)

Using the find command will make it easy to avoid listing source files manually, which often leads to bugs by forgetting to add some file to the Makefile.

This command is extremely helpful and comes in handly a lot of times, so it's best to give its manual page a read to make more complex builds in the future.

$ man find

Recipes

We can incorporate the following expesions in our recipes:

Expression Expands to
$(@D) Directory of the target
$@ Target
$^ All dependencies
$< First dependency

Like so:

all: $(INCLUDE) $(LIB) $(BIN)

# Compile the object files
cache/%.o: src/%.c
  @mkdir -p $(@D)
  $(CC) $^ -o $@ -c

# Copy the include files
build/include/$(TARGET)/%.h: src/%.h
  @mkdir -p $(@D)
  cp $^ $@

# Build our library using the cached object files
$(LIB): $(OBJ)
  @mkdir -p $(@D)
  $(CC) $^ -o $@ -shared

# Build our executable using only our library
# Note that `$<` only matches the first dependency
$(BIN): src/main.c $(LIB)
  @mkdir -p $(@D)
  $(CC) $< -o $@ -l$(TARGET) -Lbuild/lib

clean:
  rm -rf build/
  rm -rf cache/

Now we can just compile.

$ make all
cp src/utils/log.h build/include/project/utils/log.h
cp src/utils/mod.h build/include/project/utils/mod.h
cp src/services/client.h build/include/project/services/client.h
cp src/services/server.h build/include/project/services/server.h
cp src/services/mod.h build/include/project/services/mod.h
cp src/lib.h build/include/project/lib.h
gcc src/utils/mod.c -o cache/utils/mod.o -c
gcc src/utils/log.c -o cache/utils/log.o -c
gcc src/services/client.c -o cache/services/client.o -c
gcc src/services/server.c -o cache/services/server.o -c
gcc src/services/mod.c -o cache/services/mod.o -c
gcc src/lib.c -o cache/lib.o -c
gcc cache/utils/mod.o cache/utils/log.o cache/services/client.o cache/services/server.o cache/services/mod.o cache/lib.o -o build/lib/libproject.so -shared
gcc src/main.c -o build/bin/project -lproject -Lbuild/lib

You can also mute the output of cp as to avoid filling the screen with copy commands:

# Copy the include files
build/include/$(TARGET)/%.h: src/%.h
  @mkdir -p $(@D)
- cp $^ $@
+ @cp $^ $@

Define the compiler

This is usually not that much of an issue if you are not making use of some compiler's unique extensions, like GCC's nested functions or Clang's blocks which are commonly used in some modern C projects for simplyfing a lot of procedures.

So to ensure the project is compiled with the C compiler you're working with you should set it up like this:

CC = clang

But to also allow for the developer to compile with any compiler of their liking by manually setting the variable in the make command you should use :=.

CC := clang

Now we can set CC manually to change our compiler.

$ make all
...
clang src/utils/mod.c -o cache/utils/mod.o -c
...
clang src/main.c -o build/bin/project -lproject -Lbuild/lib
$ make all CC=gcc
...
gcc src/utils/mod.c -o cache/utils/mod.o -c
...
gcc src/main.c -o build/bin/project -lproject -Lbuild/lib

Define flags

This is very straight forward and mostly just part of C fundamentals, but here's the basic usage of the flags:

C_FLAGS = -Ofast -Wall -pedantic

ifeq ($(CC), gcc)
  C_FLAGS += -Wformat -Wlogical-op -fstrength-reduce
else ifeq ($(CC), clang)
  C_FLAGS += -fstack-protector
endif

cache/%.o: src/%.c
  @mkdir -p $(@D)
  $(CC) $(C_FLAGS) $^ -o $@ -c

build/include/$(TARGET)/%.h: src/%.h
  @mkdir -p $(@D)
  cp $^ $@

$(LIB): $(OBJ)
  @mkdir -p $(@D)
  $(CC) $(C_FLAGS) $^ -o $@ -shared

$(BIN): src/main.c $(LIB)
  @mkdir -p $(@D)
  $(CC) $(C_FLAGS) $< -o $@ -l$(TARGET) -Lbuild/lib
$ make all
...
clang -Ofast -Wall -pedantic -fstack-protector-all src/utils/mod.c -o cache/utils/mod.o -c
...
clang -Ofast -Wall -pedantic -fstack-protector-all src/main.c -o build/bin/project -lproject -Lbuild/lib
$ make all CC=gcc
...
gcc -Ofast -Wall -pedantic -Wformat -Wlogical-op -fstrength-reduce src/utils/mod.c -o cache/utils/mod.o -c
...
gcc -Ofast -Wall -pedantic -Wformat -Wlogical-op -fstrength-reduce src/main.c -o build/bin/project -lproject -Lbuild/lib

Secondary expansion

Secondary expansion allows us to make use of some amazing tricks (as seen in the hyperlink), but the one that's simplest and nicest to look at is definitely expanding $(@D) in the order-only-prerequisites section.

By adding a recipe for directories we can make it so that they are created only once and avoid manually creating them inside the recipe of all our targets.

# Enabling feature
.SECONDEXPANSION:

# Compile the object files
cache/%.o: src/%.c | $$(@D)/
  $(CC) $^ -o $@ -c

# Copy the include files
build/include/$(TARGET)/%.h: src/%.h | $$(@D)/
  cp $^ $@

# Build our library using the cached object files
$(LIB): $(OBJ) | $$(@D)/
  $(CC) $^ -o $@ -shared

# Build our executable using only our library
# Note that `$<` only matches the first dependency
$(BIN): src/main.c $(LIB) | $$(@D)/
  $(CC) $< -o $@ -l$(TARGET) -Lbuild/lib

# Directory recipe
%/:
  mkdir -p $@
$ make all
mkdir -p build/include/project/utils/
cp src/utils/log.h build/include/project/utils/log.h
cp src/utils/mod.h build/include/project/utils/mod.h
mkdir -p build/include/project/services/
cp src/services/client.h build/include/project/services/client.h
cp src/services/server.h build/include/project/services/server.h
cp src/services/mod.h build/include/project/services/mod.h
cp src/lib.h build/include/project/lib.h
mkdir -p cache/utils/
gcc src/utils/mod.c -o cache/utils/mod.o -c
gcc src/utils/log.c -o cache/utils/log.o -c
mkdir -p cache/services/
gcc src/services/client.c -o cache/services/client.o -c
gcc src/services/server.c -o cache/services/server.o -c
gcc src/services/mod.c -o cache/services/mod.o -c
gcc src/lib.c -o cache/lib.o -c
mkdir -p build/lib/
gcc cache/utils/mod.o cache/utils/log.o cache/services/client.o cache/services/server.o cache/services/mod.o cache/lib.o -o build/lib/libproject.so -shared
mkdir -p build/bin/
gcc src/main.c -o build/bin/project -lproject -Lbuild/lib

Now we can be pedantic printing the directories created, or we can also just mute mkdir to get a cleaner output.

%/:
- mkdir -p $@
+ @mkdir -p $@

The final product

Adding everything up from this article we end up with this Makefile:

CC := clang

C_FLAGS = -Ofast -Wall -pedantic

ifeq ($(CC), gcc)
C_FLAGS += -Wformat -Wlogical-op -fstrength-reduce
else ifeq ($(CC), clang)
C_FLAGS += -fstack-protector-all
endif

TARGET = project

SRC = $(shell find src/ -name '*.c' ! -name 'main.c')
INC = $(shell find src/ -name '*.h')

OBJ = $(SRC:src/%.c=cache/%.o)

INCLUDE = $(INC:src/%.h=build/include/$(TARGET)/%.h)
LIB = build/lib/lib$(TARGET).so
BIN = build/bin/$(TARGET)

all: $(INCLUDE) $(LIB) $(BIN)

.SECONDEXPANSION:

cache/%.o: src/%.c | $$(@D)/
  $(CC) $(C_FLAGS) $^ -o $@ -c

build/include/$(TARGET)/%.h: src/%.h | $$(@D)/
  @cp $^ $@

$(LIB): $(OBJ) | $$(@D)/
  $(CC) $(C_FLAGS) $^ -o $@ -shared

$(BIN): src/main.c $(LIB) | $$(@D)/
  $(CC) $(C_FLAGS) $< -o $@ -l$(TARGET) -Lbuild/lib

%/:
  @mkdir -p $@

clean:
  rm -rf build/
  rm -rf cache/
C
Make
GNU

Build Zig with Nix

2025-09-28
42

Network operations are not possible when building a Nix package, therefore Zig fetching the package dependencies to build your program or application will not work and instead you need to store your pre-downloaded dependencies in the cache Zig is gonna use. Here are a couple ways to do so.

Zon2Nix

Zon2Nix is a program that will convert the dependencies in build.zig.zon to a Nix expression.

Basic build file

Our most basic default.nix for our Zig application must look like this:

# default.nix
{
  pkgs ? import <nixpkgs> {},
  # unstable channel to build with the latest Zig release
  unstable ? import (fetchTarball "https://channels.nixos.org/nixos-unstable/nixexprs.tar.xz") {},
}:
pkgs.stdenv.mkDerivation {
  pname = "my-program";
  version = "0.0.0";
  
  src = ./.;
  
  buildInputs = [
    unstable.zig
  ];
  
  patchPhase = ''
    export HOME="$TMPDIR"
    export ZIG_GLOBAL_CACHE_DIR="$HOME/.cache/zig"
    mkdir -p "$ZIG_GLOBAL_CACHE_DIR"
  '';
  
  buildPhase = ''
    zig build --prefix $out --release=fast
  '';
  
  installPhase = ''
    zig build --prefix $out --release=fast install
  '';
  
  meta = {
    homepage = "https://my-project.com";
    description = "My project description";
    license = pkgs.lib.licenses.my-license;
    mainProgram = "my-program";
  };
}

Generate dependencies file

Run:

zon2nix > deps.nix

Which generates a deps.nix file like this:

{ linkFarm, fetchzip }:
linkFarm "zig-packages" [
  {
    name = "<HASH>";
    path = fetchzip {
      url = "<URL>";
      hash = "<NIX-HASH>";
    };
  }
  # ...
]

The we import it into our default.nix file with pkgs.callPackage ./deps.nix {}

# default.nix
{
  pkgs ? import <nixpkgs> {},
  # unstable channel to build with the latest Zig release
  unstable ? import (fetchTarball "https://channels.nixos.org/nixos-unstable/nixexprs.tar.xz") {},
+ # our dependencies link farm import
+ deps ? pkgs.callPackage ./deps.nix {};
}:
pkgs.stdenv.mkDerivation {

...

  patchPhase = ''
    export HOME="$TMPDIR"
    export ZIG_GLOBAL_CACHE_DIR="$HOME/.cache/zig"
    mkdir -p "$ZIG_GLOBAL_CACHE_DIR"
+   
+   ln -s ${deps} "$ZIG_GLOBAL_CACHE_DIR/p"
  '';

...

And done!

Zig
Nix