2 releases
new 0.1.1 | Dec 21, 2024 |
---|---|
0.1.0 | Dec 21, 2024 |
#56 in Build Utils
105KB
803 lines
A simple tool that makes self-contained abominations of C code called bundles. It takes many .c
and .h
files and concatenates figures out the dependencies between them. Then, using patented (not yet) dependency finding algorithms, it arranges the source code in the files in the correct order so as to produce a single .c
file that contains everything. In other words, given a bunch of header and implementation files, this tool can produce a single .c
file that (hopefully) compiles and works the same way as just compiling each translation unit by itself and then linking them together.
Table of Contents
Usage
The tool operates as a simple preprocessor. Like the C preprocessor, the tool has its own directives that instruct it how to bundle code together. Enough words. Here is an example:
Consider the following C code (don't worry about the // cbundl
comments for now):
frob.h
#ifndef _FOO_H
#define _FOO_H
struct frobinator {
int frob_count;
};
void frobinate(struct frobinator* frob);
#endif
// cbundl: impl=frob.c
frob.c
// cbundl: bundle
#include "frob.h"
#include <stdio.h>
static void update_frob_count(int *frob_count) {
*frob_count += 1;
}
void frobinate(struct frobinator* frob) {
printf("frobbed!\n");
update_frob_count(&frob->frob_count);
}
main.c
#include <stdio.h>
// cbundl: bundle
#include "frob.h"
int main() {
struct frobinator f = {0};
for (int i = 0; i < 10; i++)
frobinate(&f);
printf("enough...\n");
return 0;
}
To compile and run this project you would have to do something along the lines of:
$ cc main.c frob.c -o frob
Which would get you a binary. Now let's just assume that you are in a fictional universe where for whatever reason your code will be compiled with a fixed command along the lines of:
$ cc main.c -o frob
Well... that won't work because the linker complains that we did not give it any implementation for frobinate()
. We cannot change the compilation command. But we can change what code goes in the compiler. If we insert a pre-processing step on our code before it is even sent to the compiler we could theoretically include the implementation of frobinate()
directly in main.c
. This way we would end up with a self-contained translation unit which the compiler (and linker) will be happy to assemble into an executable. We can use cbundl
for exactly this.
$ cbundl main.c -o final.c
The above command will parse main.c
and figure out what dependencies it has. In this example, main.c
wants stdio.h
and frob.h
. Notice the comment above the #include "frob.h"
. Comments that begin with // cbundl:
are called "directives" and give special instructions to cbundl
. The directive above frob.h
tells cbundl
that, to build the final bundle, it needs to include frob.h
. The directive at the end of frob.h
tells cbundl
that the implementation for at least one of the symbols declared by frob.h
lives in frob.c
. This tells cbundl
to include frob.c
inside the resulting bundle. That's it. That's the entire tool 👏. The file final.c
then contains:
/**
*
* ) ( (
* ( /( ( )\ ) )\
* ( )\()) ))\ ( (()/(((_)
* )\ ((_)\ /((_) )\ ) ((_))_
* ((_)| |(_)(_))( _(_/( _| || |
* / _| | '_ \| || || ' \))/ _` || |
* \__| |_.__/ \_,_||_||_| \__,_||_|
*
* cbundl X.X.X-release (XXXXXXX)
* https://github.com/threadexio/cbundl
*
* Generated at: XXX XX XXX XXX XX:XX:XX (UTC+XX:XX)
*
*/
/**
* bundled from "frob.h"
*/
#ifndef _FOO_H
#define _FOO_H
struct frobinator {
int frob_count;
};
void frobinate(struct frobinator* frob);
#endif
/**
* bundled from "main.c"
*/
#include <stdio.h>
int main() {
struct frobinator f = {0};
for (int i = 0; i < 10; i++) frobinate(&f);
printf("enough...\n");
return 0;
}
/**
* bundled from "frob.c"
*/
#include <stdio.h>
static void update_frob_count(int* frob_count) { *frob_count += 1; }
void frobinate(struct frobinator* frob) {
printf("frobbed!\n");
update_frob_count(&frob->frob_count);
}
The compiler is now happy to make us our binary. 😃 Congratulations, you now know everything about this tool. 👏 And because we were good programmer boys, girls and everything in between, cbundl
will also pass the resulting bundle code through a code formatter of our choice (clang-format
by default) so its nice and pretty.
Command line arguments
Usage: cbundl [OPTIONS] <path>
Arguments:
<path>
Path to the entry source file.
Options:
--no-format
Don't pass the resulting bundle through the formatter.
--formatter <exe>
Code formatter. Must format the code from stdin and write it to stdout.
[default: clang-format]
--deterministic
Output a deterministic bundle.
-o, --output <path>
Specify where to write the resulting bundle.
[default: -]
-h, --help
Print help (see a summary with '-h')
-V, --version
Print version
Directives
Directives are special single-line comments (//
, not /* */
) that give instructions to cbundl
.
The format of directives is as follows:
// cbundl: <body>
The directive means different things depending on what <body>
is. At this time, only 2 different directives exist:
bundle
impl
bundle
Format: // cbundl: bundle
The bundle directive must always appear exactly above a local #include
, without any other comments or code in between. It informs cbundl
of a dependency relation between the current file and the #include
d file. An intuitive way to think about it, is that the current file "wants" the #include
d file. Any #include
s annotated with a bundle directive will not appear in the bundle. Additionally, any #include
s not annotated with a bundle directive will be left as-is. This allows you to create a kind of semi-bundle where even the final bundle includes local files. I can't imagine where that would be useful, but you can do it.
impl
Format: // cbundl: impl=<path>
The impl
directive, also called an implementation directive, informs cbundl
that the current file is implemented by the file specified by <path>
. This directive can appear any number of times in the file (if the implementation is split across many other files). It can also appear anywhere in the file, but convention is that impl
directives appear only at either the start or the end of the file. Just like #include
-ing .c
files, using an implementation directive that points to a .h
file is generally considered bad practice.
Workflow
Ok that's all cool and all but how do I integrate it into my workflow? I'm glad you asked. Simple, instead of running just:
$ cc ...
You do:
$ cbundl main.c > bundle.c
$ cc bundle.c -o main
Just make the bundle with cbundl
and compile the bundle instead of your source files. You can also write a simple Makefile
that does this:
build:
cbundl main.c > bundle.c
cc bundle.c -o main
Installation
cbundl
provides pre-built release binaries in Releases for all 3 major desktop platforms.
[!NOTE] Those binaries are built in Github Actions. However if you still don't trust the binaries, I don't blame you. Proceed to the Building section.
cargo
If you happen to have cargo
installed you can simply do:
$ cargo install cbundl
manually
cbundl
is a standalone binary. This means you can download (or build) the appropriate binary for your system and place it in a location included in PATH
and be done. You can install cbundl
only for your own user by doing:
$ mkdir ~/.bin
$ export PATH="~/.bin:$PATH"
[!NOTE] The above will probably not work on non-Bourne shells.
You can then download the latest cbundl
binary to ~/.bin
from Releases. Then if you restart your shell, it should be immediately available. You can check with cbundl --version
.
Building
Ironic that a C source code pre-processing tool is not written in C, isn't it? Anyway, as cbundl
is written in a modern language called "Rust", you don't have to fiddle with finicky Makefile
s or an esoteric cmake
setup to get the damn thing to build. Simply do:
$ cargo build # for the debug build
# or
$ cargo build --release # for the release build
Then you can run cbundl
through cargo
with cargo run
or by running it directly from target/debug/cbundl
or target/release/cbundl
, depending on which you built.
License
- All code and contributions in this repository are licensed under the Apache 2.0 license, a copy of which can be found here.
- All artwork in this repository is licensed under Creative Commons Attribution-NonCommercial 4.0 International. A copy of the license can be found here.
Dependencies
~8–14MB
~167K SLoC