gabrielzschmitz / Tomato.C

A pomodoro timer written in pure C.
GNU General Public License v3.0
315 stars 19 forks source link

Tips for project organisation #6

Closed z3bra closed 1 year ago

z3bra commented 1 year ago

Hey ! As discussed, here are a few pointers on how I think you can improve your project.

These tips come after many years as both a C developper, and as a packager for multiple distro.

Disclaimer: This is very very VERY opiniated. I've been doing that for my own projects for years now, but your mileage may vary. Take it all as advices from a fellow C programmer, and not as hard rules to set in stone. As always, do your own research, and seek differents points of views to make your own opinion !

Makefile

The makefile is the entrypoint for both developpers and packagers. As such, you should standardize its format to make their life easier when using it to test the program, or package it.

Note that there are bunch of tips you already follow somehow, which is good, keep it up! I'll put details behind them so you also know why I think it's good to do it.

0. Avoid cmake and autotools

This is very opiniated! I prefer manually crafted Makefile that I can easily fix when they break, to procedurally generated one that you cannot understand or fix.

1. Use widespread rule names

A few key words are implicitely agreed on, and you should include them in all your makefiles:

There are other similar rules like mrproper, distclean, release, but they are much less common, and IMO make the file longer than it should be. Make sure to include at least the 3 rules above.

2. Use widespread macros

Just like rule names, some macros can be used to alter how the program should behave. Some of them are even built-in the make(1) program, so you should definitely include them as well:

Ok, that's a lot of them. Lets first look at those that are not built into make(1): DESTDIR, PREFIX, MANPREFIX.

These rules serve the same purpose: installing the program.

PREFIX and MANPREFIX define where to install the binaries and documentation. Basically, /usr/local and /usr/local/man by default.

DESTDIR, however, serves a different purpose. You can see it more like a "chroot" option. This is used by packagers mostly, so they can install the program in a subdirectory, and generate a tarball of the installed package for example.

Note that setting DESTDIR = /tmp/chroot and PREFIX = /usr/local is different than only setting PREFIX = /tmp/chroot/usr/local. Some program reuse the PREFIX macro to do other stuff (eg. generate documentation or pkg-config files), and as such DESTDIR allow you to change the target path of the binary without changing the final path.

from the others: CC, LD, CPPFLAGS, CFLAGS, LDFLAGS, LDLIBS, this leads us to my next point:

3. Use implicit rules

Again, very opiniated. And to be honest, I'm leaning more toward the "write down everything explicitely" now that I use mk more than make). However it's important to know that they exist, and to use them when appropriate.

These are rules that are built into the program for common scenarios, like compiling C source files into objects, and linking objects to binaries. These rules also make use of predefined macros to let you change their behavior without having to rewrite those rules.

Here is what they look like:

# Compiles any .c file into the corresponding .o
# Uses CC, CFLAGS and CPPFLAGS macros
.c.o:
        $(CC) $(CFLAGS) $(CPPFLAGS) -c -o $@ $<

# Link a .o file into a binary
# Uses LD, LDFLAGS and LDLIBS macros
.o:
    $(LD) $(LDFLAGS) $(LDLIBS) -o $@ $<

This means that by writing your makefile carefully, you do not have to write how to actually compile files, just how they depend on each others.

Specifying dependencies in a makefile is pretty simple:

target...: dependencies...

And you can add as much as you want, on either side of the ":", and repeat them multiple time if needed.

Here is an example. This makefile as a whole is totally valid, and running make would do exactly what you expect: produce the program binary, all using implicit rules, and recompile what's needed when you update source files:

program: file1.o file2.o
file1.o file2.o: util.h config.h
file1.o: file1.h
file2.o: file2.h

As an exercise, try to figure out why the following works:

% cat <<EOF > hello.c
> #include <stdio.h>
> int main() {
>   return printf("Hello, World!\n") > 0;
> }
> EOF
% ls
hello.c
% make hello
cc     hello.c   -o hello
% ./hello
Hello, World!

4. Feature a config.mk file

This is a file where you put all your macros definitions (CFLAGS, LDLIBS, …). Different systems require different configurations, and having these macros in their own file makes overriding it easier, rather than using sed -i to edit the Makefile directly, or to patch it.

Some people would disagree, but as a packager, it makes my life easier when projects do this (especially when they provide multiple ones per OS, eg. config.mk.OpenBSD, config.mk.Debian, …)

You can then include the file from your makefile:

include config.mk

Note: You can add a "-" at the beginning ("-include config.mk") to prevent getting an error if the file doesn't exists.

TL;DR: My take on your Makefile

config.mk

PREFIX  = /usr/local
CPPFLAGS = -I/usr/local/include
CFLAGS  = -Wall -Wextra -pedantic
LDFLAGS = -L/usr/local/lib
LDLIBS  = -lncursesw

Makefile

-include config.mk

tomato: tomato.o util.o input.o update.o anim.o
tomato.o: util.h input.h update.h anim.h
util.o: util.h
anim.o: anim.h
input.o: input.h
update.o: update.h

clean:
    rm -f tomato *.o

install: tomato
    mkdir -p ${DESTDIR}${PREFIX}/bin
    cp tomato ${DESTDIR}${PREFIX}/bin/tomato

uninstall:
    rm -f ${DESTDIR}${PREFIX}/bin/tomato
gabrielzschmitz commented 1 year ago

Man, I couldn't be more thankful :smiley: I'll implement all the tips! I want to add a notification sound, but not sure where to put it, could you give me a insight? What I've done at the moment:

    mkdir -p ${PREFIX}/share/tomato
    cp -f notification.mp3 notification2.mp3 ${PREFIX}/share/tomato

Is it right or should I place it in another folder?

z3bra commented 1 year ago

${PREFIX}/share/NAME is a good place for "assets". I would add another subdir (eg. "sounds" or "audio") though, so it's not just a flat directory with everything if you ever decide to add other stuff.