πŸ”Ά Manage a Makefile project πŸ†•

While mulle-sde is most powerful, when setting up new projects, it can also be usefully employed, with an existing project. To illustrate this, a small demo project is created and then lightly adapted to work seamlessly with mulle-sde.

Setup of the demo project

It’s going to be a C based make project, if that doesn’t interest you and you prefer cmake or meson, you can skip to Employ mulle-sde to get the custom library and continue from there.

Let AI create a demo Makefile

Let an AI (like ChatGPT) create a simple Makefile for an executable, in a form that could be considered somewhat standard [1]:

# Compiler and linker flags
CC = gcc
CFLAGS = -Wall -Wextra
LDFLAGS = -L/path/to/lib -lmulle-core
INCLUDES = -I/path/to/includes

# Variables for installation prefixes, extra include directories, and library directories
PREFIX = /usr/local
INCDIR = $(PREFIX)/include
LIBDIR = $(PREFIX)/lib

# Source and object files
SRCS = my.c other.c
OBJS = $(SRCS:.c=.o)
EXECUTABLE = my.exe

# Default target
all: $(EXECUTABLE)

# Rule for building the executable
$(EXECUTABLE): $(OBJS)
   $(CC) $(LDFLAGS) $(INCLUDES) -o $@ $^

# Rule for building the object files
%.o: %.c
   $(CC) $(CFLAGS) $(INCLUDES) -c $< -o $@

# Install target
install: $(EXECUTABLE)
   cp $(EXECUTABLE) $(PREFIX)/bin
   cp my.h $(INCDIR)
   cp libmulle-core.so $(LIBDIR)

# Clean target
clean:
   rm -f $(OBJS) $(EXECUTABLE)

Let AI create demo content

I am not sure why the AI wants a header for an executable, but lets play along anyway [2]:

For my.h:

#ifndef MY_H
#define MY_H

void print_message();

#endif /* MY_H */

For my.c:

#include "my.h"
#include <stdio.h>

void print_message() {
   printf("Hello, World!\n");
}

For other.c:

#include "my.h"

int main() {
   print_message();
   return 0;
}

AI: In this example, my.h is a header file that declares a function prototype for print_message(). my.c is a source file that defines print_message(), and other.c is another source file that uses print_message().

Employ mulle-sde

Build project with mulle-make

Run mulle-make instead of plain make. What’s the advantage ? mulle-make will automatically create a suitable -j argument to parallelize the build. Also the output of make will be logged separately into a log file in a build folder for later perusal with (mulle-make log).

We do error out though, because -lmulle-core isn’t installed yet (usually):

$ mulle-make
collect2: error: ld returned 1 exit status
make: *** [Makefile:25: my.exe] Fehler 1
/usr/bin/ld: -lmulle-core kann nicht gefunden werden: Datei oder Verzeichnis nicht gefunden
collect2: error: ld returned 1 exit status
make: *** [Makefile:25: my.exe] Fehler 1
mulle-make fatal error: make failed with 2 (/tmp/xya)

As an intermediate step though, before fixing the link error, it would be nicer, if all .o files would also end up in the build folder, so we get parts of the Makefile rewritten by the AI [3] too:

...

# Build directory
BUILD_DIR = build

# Source and object files
SRCS = my.c other.c
OBJS = $(addprefix $(BUILD_DIR)/, $(SRCS:.c=.o))
EXECUTABLE = my.exe

...

# Clean target
clean:
   rm -rf $(BUILD_DIR) $(EXECUTABLE)

Now let’s get the -lmulle-core working.

Employ mulle-sde to get the custom library

To get dependencies (third party libraries) happening, we need to initialize this project for mulle-sde. This will add a .mulle folder to the project. As the project already exists, we use:

mulle-sde init none

The project type β€˜none’ will add no additional files outside of the .mulle folder and mulle-sde will not attempt to build the main project.

Now we will link our executable with mulle-core. mulle-core is nice for demo purposes, because its known to work :) But you can try out non-mulle libraries as well. If they use autoconf or cmake, chances are good, that they work without tweaks.

mulle-sde add github:mulle-core/mulle-core  # add .tar suffix to fetch archives

What can be added ?

In principle anything can be added that is based on cmake or autoconf. meson and make projects can work too. Everything can be made to fit using additional script though. It needn’t be hosted on github, though github is best supported.

So mulle-sde should have cloned the mulle-core repository from github and already compiled it. But where is it ? It’s in the dependency/Debug folder. A release build would end up in the root of the dependency (with default settings). That’s more conventional, so lets do a β€œRelease” build instead and remove the dependency/Debug folder:

mulle-sde clean tidy
mulle-sde -v craft --release  # use the -v flag to see log during build

Now we have to adjust the Makefile so that the include and library directories in dependency are found [4]:

...

DEPENDENCY_DIR = dependency

# Default target
all: $(EXECUTABLE)

# Rule for building the executable
$(EXECUTABLE): $(OBJS)
   $(CC) -L$(DEPENDENCY_DIR)/lib -lmulle-core -I$(DEPENDENCY_DIR)/include -o $@ $^

# Rule for building the object files
$(BUILD_DIR)/%.o: %.c
   $(CC) $(CFLAGS) -I$(DEPENDENCY_DIR)/include -c $< -o $@

# Install target
install: $(EXECUTABLE)
   cp $(EXECUTABLE) $(PREFIX)/bin
   cp my.h $(INCDIR)
   cp $(DEPENDENCY_DIR)/lib/libmulle-core.so $(LIBDIR)

...

I am not a super-fan of the way the AI coded this, but fine enough. Lets run mulle-make again, and there should be a my.exe in the project directory.

Tie mulle-make and mulle-sde together

Now we have mulle-sde craft to build the dependency and mulle-make to build the project. Lets tie both together, so that mulle-sde craft can do both.

# turn on main project builds
mulle-sde env --project remove MULLE_SDE_CRAFT_TARGET
mulle-sde env --project set PROJECT_TYPE 'executable'
# ensure that dependency is built 'Release'
mulle-sde env --this-user set MULLE_SDE_CRAFTORDER_STYLE 'Release'

Now the project can be built with mulle-sde. But can it still be built on remote systems, where there is only a make and mulle-core installed but no mulle-sde ?

Yes, with a little tweak of the Makefile [5]:

# Compiler and linker flags
CC = gcc
CFLAGS = -Wall -Wextra

# Variables for installation prefixes, extra include directories, and library directories
PREFIX = /usr/local
INCDIR = $(PREFIX)/include
LIBDIR = $(PREFIX)/lib

# Build directory
BUILD_DIR = build

# Dependency directory
DEPENDENCY_DIR = dependency

# Library and include directories
LIB_DIR = $(DEPENDENCY_DIR)/lib
INC_DIR = $(DEPENDENCY_DIR)/include

# Check if the dependency directory exists
ifeq ($(wildcard $(DEPENDENCY_DIR)),$(DEPENDENCY_DIR))
   LIB_DIR = $(DEPENDENCY_DIR)/lib
   INC_DIR = $(DEPENDENCY_DIR)/include
else
   LIB_DIR = /usr/lib
   INC_DIR = /usr/include
endif

# Source and object files
SRCS = my.c other.c
OBJS = $(addprefix $(BUILD_DIR)/, $(SRCS:.c=.o))
EXECUTABLE = my.exe

# Default target
all: $(EXECUTABLE)

# Rule for building the executable
$(EXECUTABLE): $(OBJS)
   $(CC) -L$(LIB_DIR) -lmulle-core -I$(INC_DIR) -o $@ $^

# Rule for building the object files
$(BUILD_DIR)/%.o: %.c
   $(CC) $(CFLAGS) -I$(INC_DIR) -c $< -o $@

# Install target
install: $(EXECUTABLE)
   cp $(EXECUTABLE) $(PREFIX)/bin
   cp my.h $(INCDIR)
   cp $(LIB_DIR)/libmulle-core.so $(LIBDIR)

# Clean target
clean:
   rm -rf $(BUILD_DIR) $(EXECUTABLE)

The AI changed things around for the better IMO.

Now we are done:

mulle-sde craft --clean-all

and that’s what you should be seeing:

Clean all
Creating xya craftorder
Let cmake/ninja do a Release build of mulle-core ...
Let make/make do a Debug build of xya .

The intermediate build files are no longer in build, instead the build is now done also in kitchen where the temporary mulle-core object files (and logs) are placed. So build is unused now and can be removed.

Spruce up the project with personal preferences

Use shell aliases to reduce type-work

As one will by typing mulle-sde craft a lot, create aliases like these:

alias sde=`mulle-sde`
alias c=`mulle-sde craft`

Separate Release and Debug directories in dependency

mulle-sde env --this-user set MULLE_CRAFT_DISPENSE_STYLE 'strict'

or do the opposite and use dispense style none to also not create a Debug directory.

Get stash, kitchen, dependency out of the way

IDEs who employ indexing, slow down a lot, when they encounter directories with build products and third party sources. There is an easy way, to move mulle-sde directories out of the project.

mulle-sde clean tidy
rm -rf kitchen stash dependency
uuid=`uuidgen`
mulle-sde env --this-host set DEPENDENCY_DIR '${HOME}/.mulle/var/${PROJECT_NAME}-'${uuid}'/dependency'
mulle-sde env --this-host set KITCHEN_DIR '${HOME}/.mulle/var/${PROJECT_NAME}-'${uuid}'/kitchen'
mulle-sde env --this-host set MULLE_SOURCETREE_STASH_DIR '${HOME}/.mulle/var/${PROJECT_NAME}-'${uuid}'/stash'

To guard against clashes with duplicate projects of the same name, use an UUID in the directory path:

The Makefile should now get the value from mulle-sde either via MULLE_SDK_PATH, which will be provided as an argument, or we could be asking: mulle-sde dependency-dir:

MULLE_SDK_PATH ?=

# Dependency directory
DEPENDENCY_DIR := $(shell echo $(MULLE_SDK_PATH) | cut -d':' -f1)

Final Words

So what was gained ?

  • project users can fetch dependencies and build and project in one step
  • dependencies don’t clutter or conflict with the system or other projects
  • the possibility of having β€œbuild local” environment variables like CC defined
  • more opportunities await, as there is a lot more to uncover about mulle-sde

With one dependency, you might shrug it off, but the more dependencies your project has, the more useful mulle-sde will become.


AI Prompts

[1]You are a C and Unix expert. Write me a Makefile with common conventions that maintains an executable "my.exe" which links against a custom library "-lmulle-core" library. We need all and install and clean targets as well. Also need some variables for installation prefixes, extra include directories and library directories.

[2] Please provide some demo content for the files in that Makefile: my.h, my.c, other.c.

[3] Can we change OBJS = $(SRCS:.c=.o) so that the output lands in a "build" folder ?

[4] The /path/to/ is really a local folder called "dependency", and the header directory in there is called "include" not "includes". Please use an additional variable so we can move "dependency" around.

[5] Please modify the Makefile so that, if there is no dependency folder in the project, that the default system /usr is searched.