Installing your C library for end users¶
Your generated project is already a distributable C library. After
cmake --install, end users who don't touch Python at all can link against
it with a single pkg-config or find_package call:
No just-makeit required on the consumer's machine. The sections below walk through prerequisites, build, install, and runtime loading.
Prerequisites¶
The end user needs the following tools — just-makeit itself is not required:
| Tool | Minimum version | Notes |
|---|---|---|
| CMake | 3.16 | Build system; drives configure + install |
| A C99 compiler | GCC 8 / Clang 10 | gcc or clang; on Windows clang-cl — MSVC's cl.exe rejects float _Complex |
| pkg-config | any | For pkg-config consumers only |
Linux (Debian/Ubuntu):
macOS (Homebrew):
brew install cmake pkg-config
# the C compiler (Apple clang; `gcc` is an alias for it) ships with the
# Xcode Command Line Tools: xcode-select --install
Windows: Visual Studio Build Tools (C++ workload) plus LLVM for
clang-cl — see Does it work on Windows?.
Overview¶
Every just-makeit project ships a lib<project>.so in addition to its Python
extensions. The same C code drives both: each component compiles once as a
CMake OBJECT library and links into both the Python .so and the combined
shared library. End users who don't use Python at all can consume it via the
standard mechanisms below.
This applies to all project layouts — standalone objects (just-makeit object),
multi-type modules (just-makeit module + just-makeit object --module), or a mix of both.
What gets installed¶
$PREFIX/
├── include/
│ └── my_project/ # every header, under the package name
│ ├── my_project.h # umbrella: #include <my_project/my_project.h>
│ ├── clib_common.h
│ ├── component_a/
│ │ └── component_a_core.h
│ └── component_b/
│ └── component_b_core.h
├── lib/
│ ├── libmy_project.so.0.1.0 # shared library (the real file)
│ ├── libmy_project.so.0.1 # -> .so.0.1.0, its soname
│ ├── libmy_project.so # -> .so.0.1, what `-lmy_project` finds
│ ├── libmy_project.a # static library
│ ├── pkgconfig/
│ │ └── my_project.pc # pkg-config descriptor
│ └── cmake/my_project/
│ ├── my_project-config.cmake
│ ├── my_project-config-version.cmake
│ └── my_project-targets*.cmake
Every file belongs to one of two install components, the split a
distribution packages from (lib<name> and lib<name>-dev):
| component | installs |
|---|---|
runtime |
the shared library a program loads: libmy_project.so.0.1.0 and its soname link (a .dll on Windows) |
dev |
everything a build needs: the headers, libmy_project.a, the libmy_project.so link, the CMake package and the .pc |
cmake --install build --component runtime --prefix stage/runtime
cmake --install build --component dev --prefix stage/dev
A plain cmake --install installs both. Tag your own install rules below
# ── End install the same way if you package them.
Build and install¶
For a non-root local install substitute any writable path, at configure time or at install time; both work:
The .pc names the prefix its files were installed under, as an absolute
path, written at install time. So cmake --install --prefix is honoured, and
under /usr pkg-config can leave out -I/usr/include and -L/usr/lib... as
it does for every system package. A CMAKE_INSTALL_LIBDIR given as an
absolute path, as Nix and Guix do, is written as that path.
Relocating an installed tree is the consumer's side of pkg-config:
| the tree was... | the consumer runs |
|---|---|
staged with DESTDIR |
PKG_CONFIG_SYSROOT_DIR=<stage> pkg-config ... |
| moved after installing | pkg-config --define-prefix ... |
A staged .pc names the real target, not the staging directory, which is
what a distribution package needs. --define-prefix derives the prefix from
where the .pc now sits, which is right for lib/pkgconfig. It is wrong for
a multiarch lib/<triplet>/pkgconfig, where pkg-config takes lib as the
prefix: that is a pkg-config limitation, so reinstall instead. The CMake
config needs neither, since it locates itself.
Versions and ABI¶
The shared library carries a versioned soname, so a release that changes the
ABI installs beside the old one instead of over it. find_package applies
the same rule when a consumer asks for a version:
| project version | soname | find_package(my_project X.Y) accepts |
|---|---|---|
0.y.z |
libmy_project.so.0.y |
0.y.* at or above the request |
x.y.z, x ≥ 1 |
libmy_project.so.x |
x.* at or above the request |
Under 0.x a minor release may break compatibility (semver), which is why
0.1 does not accept 0.2. The version is project(... VERSION) in the root
CMakeLists.txt. The .pc's Description: and URL: come from that same
project() call's DESCRIPTION and HOMEPAGE_URL.
That version is written from [project] version when the project is created
and is not rewritten after, like pyproject.toml and <pkg>_version() in
native/src/<pkg>_lib.c: bump each with the manifest. jm status names any
copy that disagrees with it.
Using with pkg-config¶
Compile a consumer:
The .pc names everything the library needs, libm included, so the
consumer adds nothing by hand. To link statically, pass the archive in place
of -lmy_project (a bare -l finds the shared library first) and let
--static add what the archive needs:
gcc $(pkg-config --cflags my_project) consumer.c \
/usr/local/lib/libmy_project.a \
$(pkg-config --static --libs my_project | sed 's/-lmy_project//') \
-o consumer
Linux /
--as-needednote: Split--cflagsand--libswith the source file between them. GNU ld on Debian/Ubuntu uses--as-neededby default, which silently drops any shared library that appears before the object files referencing it. If you merge them with the source last ($(pkg-config --cflags --libs my_project) consumer.c) you will get undefined-reference errors at link time even though the library is present.
If you installed to a non-standard prefix, point pkg-config at it:
Using with CMake¶
cmake_minimum_required(VERSION 3.16)
project(my_consumer C)
find_package(my_project REQUIRED)
add_executable(consumer consumer.c)
target_link_libraries(consumer PRIVATE my_project::my_project)
my_project::my_project is the shared library and
my_project::my_project-static the static one. Each carries its own link
interface, libm included, so the consumer names nothing else.
Configure with the prefix if it's not on the default search path:
A consumer can also build against the library's build directory with nothing installed, because the build tree exports the same targets:
When your library depends on another package¶
A component that calls into another C library links it with
extra_link_libs, and that package is declared once, on [project]. Declare
it with the name each consumer face needs:
[project]
find_packages = [
{ name = "Doppler", pkg_config = "doppler" }, # ships a CMake config and a .pc
{ name = "Threads", libs_private = "-pthread" }, # no .pc of its own
{ name = "Kiss", cflags = "-I/opt/kiss/include", libs_private = "-L/opt/kiss/lib -lkiss" },
]
pkg_modules = ["fftw3f", "zlib >= 1.2"] # found through pkg-config
[tone]
extra_link_libs = ["doppler::doppler-static"]
nameis the CMake package. The root callsfind_package(Doppler), and the installedmy_project-config.cmakecallsfind_dependency(Doppler).pkg_configis the dependency's pkg-config module. The installed.pclists it asRequires.private. A CMake package name says nothing about its module name (PNGislibpng,CURLislibcurl), so this is the one fact you state rather than jm derives.libs_privateis for a dependency that ships no.pc. Its flags go to the.pc'sLibs.private, pkg-config's field for exactly that case.cflagsis the compile half of the same case. When one of your headers includes a header of a dependency with no.pc, a consumer needs its include flags to compile yours. pc(5) has no private Cflags, because a header's includes are needed however the consumer links, so these go on the.pc's ownCflagsline.pkg_modulesentries are already pkg-config module names, so they reachRequires.privatewith nothing more to say. An entry may carry a version bound the way pc(5) writes one: a name, then one of=,<,>,<=,>=and a version ("zlib >= 1.2"). The name alone gives the target (PkgConfig::ZLIB). The bound reachespkg_check_modulesin the root and in the installed config, and the.pc'sRequires.private.jm applyrefuses any other spelling.
With that, a consumer of the installed project gets the dependency on every face, and names nothing but your project:
| consumer | compiling your headers | linking |
|---|---|---|
find_package, shared |
the dependency's include dirs and flags | already resolved inside libmy_project.so |
find_package, static |
the same | the dependency, through the link interface |
pkg-config, shared |
its Cflags via Requires.private, or your cflags |
already resolved inside libmy_project.so |
pkg-config --static |
the same | its Libs, or your libs_private |
"Compiling your headers" matters whenever a header of yours includes one of
the dependency's, as nco_tone's tone_core.h includes doppler's
doppler/nco/nco_core.h. The consumer still needs the dependency
installed: put its prefix on CMAKE_PREFIX_PATH or PKG_CONFIG_PATH
beside yours.
A bare string entry (find_packages = ["Doppler"]) still works through
find_package, but the .pc cannot name it: a pkg-config consumer then gets
no Cflags for it, and no libs when linking static. jm status lists such
entries under PKG-CONFIG, so the gap is never silent.
Link a package's imported target (doppler::doppler-static,
PkgConfig::FFTW3F) rather than a path or a ${VAR} holding one. Only a
target carries the include dirs and flags a consumer needs, and a path would
put this machine's layout into the installed package.
Flags your headers need of every consumer¶
Some needs have no package to declare. An installed header with an inline
function that calls pthread makes every consumer's own object call it, so
every consumer links -lpthread. A header that names MAP_ANONYMOUS or calls
syscall() compiles under -std=c99 only if _GNU_SOURCE is defined before
libc is first reached, so that definition has to be on every consumer's
compile line:
[project]
public_link_libs = ["-lpthread"] # one linker flag per entry
public_defines = ["_GNU_SOURCE"] # an identifier, optionally =value; no -D
Each reaches every face, from this manifest alone:
| face | public_link_libs |
public_defines |
|---|---|---|
| this project's own build | its tests, benchmarks and Python extensions link it | every target compiles with it |
find_package |
the exported targets' link interface, shared and static | the exported targets' compile definitions |
| pkg-config | the .pc's Libs: -- public, like -lm: the consumer's object calls it |
the .pc's Cflags:, as -D<def> |
An entry of public_link_libs is one linker flag (-lpthread, -pthread,
-ldl), the spelling CMake and pkg-config read the same. A package with a
CMake config or a .pc of its own (Threads::Threads) is a dependency, not a
flag: declare it in find_packages above.
More than one library¶
A project installs lib<pkg> and, for each [project.libraries.<name>]
table, one more library built from the OBJECT libraries it names. It is how a
project ships an optional layer (a network transport, say) that a consumer who
does not need it never links:
[project.libraries.stream]
cores = ["stream_core_obj"] # OBJECT libraries, by CMake target name
description = "the stream layer"
platforms = ["linux", "macos"] # optional; built only there
| face | lib<pkg> |
[project.libraries.stream] |
|---|---|---|
| file | lib<pkg>.so / .a |
lib<pkg>_stream.so / .a |
| pkg-config | <pkg> |
<pkg>_stream, with Requires: <pkg> |
| CMake | <pkg>::<pkg>, <pkg>::<pkg>-static |
<pkg>::stream, <pkg>::stream-static |
It links lib<pkg> publicly, so a consumer that names only the additional
library gets both, by either route:
Each library gets everything lib<pkg> gets -- its soname, its install name,
its runtime/dev install components, its .pc -- from the same rules. A library
declared for other platforms is not built, installed or exported where it does
not apply, and find_package(<pkg> COMPONENTS <name>) does not find it there.
A core belongs to one library: apply refuses one also folded into
lib<pkg>, one claimed by two libraries, and one the tree does not declare as
an OBJECT library. jm never folds a claimed core into lib<pkg> itself.
Two packages, one program¶
Headers under <pkg>/ keep two installed jm packages' FILES apart, but not
their C symbols: two packages that both have a component fir both export
fir_create, and a translation unit including both fir_core.hs gets only
the first -- the shared FIR_CORE_H guard silently drops the second.
[project] c_prefix namespaces what jm derives:
| moves (derived by jm) | stays (named by you, or not C) |
|---|---|
fir_create, fir_destroy, fir_reset, fir_step(s) -> dp_fir_... |
a manifest fn =, create_fn, bridge_fn, type_name, record_dtype |
a method's fir_<name>, a property's fir_get_<name> / fir_set_<name> / fir_num_<name> |
your own macros in the sacred _core.h (ACC_F32_STATE_MAGIC) |
the fir_state_t type -> dp_fir_state_t |
the Python names: Fir, from pkg import fir, a module function mix |
jm's include guards (DP_FIR_CORE_H) and process_global defines |
file names: native/inc/<pkg>/fir/fir_core.h stays |
a module function mix -> dp_mix (in C only) |
<pkg>_version |
A name that already starts with dp_ is not prefixed again: a component
dp_tlm keeps dp_tlm_create. So apply refuses a project in which two
names derive one symbol -- x and dp_x both deriving dp_x_create, or a
method and a module function that meet -- naming both.
A consumer includes both packages' headers and calls each by its prefix:
#include "pa/fir/fir_core.h"
#include "pb/fir/fir_core.h"
pa_fir_state_t *a = pa_fir_create (2.0);
pb_fir_state_t *b = pb_fir_create (2.0);
A new project has one by default: jm new dsp writes c_prefix = "dsp", so its symbols are dsp_<comp>_* from the first file. Pick a
shorter one with jm new --c-prefix dp, or keep today's bare names with
--no-c-prefix. A manifest you write by hand spells a sibling component's
derived names prefixed too -- type = "dsp_lfo_state_t *",
create_impl = "obj->osc = dsp_lfo_create(...)".
A project made before this default has no key, and keeps its bare names
until you add one. Adding it renames every
derived symbol, which your C (the sacred _core.h / _core.c, module
function sources, tests and benchmarks) must follow: apply refuses, naming
each file and the old names it still spells, until jm upgrade respells
them (upgrading). It is also an ABI
change for your consumers -- say so in your release notes.
Calling it from C++11¶
Every generated header carries an extern "C" guard, so a C++ translation
unit can include it and call the C core directly — the mixed case where some
of your algorithms are C99 and some are C++11:
#include "<pkg>/engine/engine_core.h" // the same header your C code includes
#include <vector>
std::vector<float _Complex> xs; // C99's type, in a C++11 container
<pkg>_engine_state_t *o = <pkg>_engine_create(2.0);
float _Complex y = <pkg>_engine_step(o, xs.at(0));
<pkg>_engine_destroy(o);
Compile the C++ side with -std=c++11 and link with the C++ driver (g++ /
clang++), which is what pulls in the C++ runtime. jm's core stays C: nothing
about your project changes, and jm generates nothing extra.
The type crosses; C99's complex arithmetic does not. I, _Complex_I,
creal() and cimag() are C-only spellings — in C++ the same <complex.h>
gives you std::complex instead. Build and inspect values with GNU's
__real__ / __imag__:
jm deliberately does not define I for C++: a macro named I collides
with almost everything.
_Complex in C++ is a GNU/Clang extension, so this needs GCC or Clang. MSVC
cannot compile the C99 core either (see Prerequisites), so it is out on both
sides rather than only this one.
Implementing a component in C++ is a different thing and jm does not do it — see gh-1149.
Runtime loading (rpath)¶
Where the loader finds the installed shared library depends on where it went.
A system prefix (/usr) needs nothing. That is where a distribution
package installs.
/usr/local, CMake's default prefix: on Debian, Ubuntu and openSUSE run
sudo ldconfig once after installing. They list /usr/local/lib in
/etc/ld.so.conf.d/, but the loader reads those directories only through its
cache, which ldconfig rebuilds. Fedora, RHEL and Arch do not list it at all
(and on Fedora-family hosts the library lands in /usr/local/lib64), so there
/usr/local is any other prefix: either use the recipe below, or add the
directory to a file under /etc/ld.so.conf.d/ and run ldconfig.
Any other prefix (~/.local, /opt/…, an unpacked tarball): give the
program an rpath to the libraries it links, at link time. That is all it
needs: each installed library finds the libraries it needs by itself, through
the RUNPATH jm installs it with -- $ORIGIN for the libraries beside it, plus
the directory of any dependency installed in another prefix. So a program
names only what it links, even when that library depends on another package.
With pkg-config, one -Wl,-rpath per package the program names:
gcc consumer.c $(pkg-config --cflags --libs my_project) \
-Wl,-rpath,"$(pkg-config --variable=libdir my_project)" \
-o consumer
With find_package, CMake already gives a program in your build tree an rpath
to every imported library it links, so it runs from there as built. To
install the program as well, keep that path in the installed binary:
To try a program without relinking it, LD_LIBRARY_PATH="$HOME/.local/lib" ./consumer also works; it is for a quick test, not for deployment.
The path lives in the library, not in the .pc. An rpath in a .pc goes
wrong in a cross build (pkgconf 1.x and 2.x prefix the sysroot onto it) and
ends up in every consumer's installed binary. A packager who wants a different
policy passes CMake's own -DCMAKE_INSTALL_RPATH=... or
-DCMAKE_INSTALL_RPATH_USE_LINK_PATH=OFF, which replace jm's defaults.
macOS needs nothing. The installed .dylib names itself by its absolute
path (its install name is <prefix>/lib/lib<pkg>.<ABI>.dylib, as Homebrew's
libraries are), so a program linked by pkg-config, find_package, a Makefile
or Xcode loads it wherever the prefix is. If you move the installed tree
afterwards, point DYLD_LIBRARY_PATH at its new lib/, or rewrite the name
with install_name_tool.
Windows has no rpath: a program finds a DLL in its own directory or on
PATH. The library's DLL is installed to the prefix's bin/; put that on
PATH, or copy the DLL beside the program.