diff --git a/Cargo.lock b/Cargo.lock index 63855f5f..fc784171 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6888,6 +6888,7 @@ dependencies = [ "datafusion-table-providers", "dotenvy", "email_address", + "flate2", "futures", "gtin-validate", "iban_validate", @@ -6919,6 +6920,8 @@ dependencies = [ "steel-decimal", "steel-derive", "tantivy", + "tar", + "tempfile", "thiserror 2.0.18", "time", "tokio", diff --git a/flake.nix b/flake.nix index c886cd74..ac01fa35 100644 --- a/flake.nix +++ b/flake.nix @@ -10,18 +10,19 @@ flake-utils.lib.eachDefaultSystem (system: let pkgs = nixpkgs.legacyPackages.${system}; + portableServer = import ./packaging/musl { + inherit pkgs; + source = self; + version = "0.8.41"; + }; in { + packages = pkgs.lib.optionalAttrs (system == "x86_64-linux") { + portable-server = portableServer; + }; + devShells.default = pkgs.mkShell { - LD_LIBRARY_PATH = pkgs.lib.makeLibraryPath [ - pkgs.openssl - pkgs.krb5 - pkgs.lz4 - pkgs.zstd - pkgs.libxml2_13.out - pkgs.zlib - pkgs.readline - ]; + LD_LIBRARY_PATH = pkgs.lib.makeLibraryPath [ pkgs.openssl ]; buildInputs = with pkgs; [ mermaid-cli @@ -51,12 +52,6 @@ # OpenSSL for crypto dependencies openssl openssl.dev - krb5 - lz4 - zstd - libxml2_13.out - zlib - readline # PostgreSQL for sqlx postgresql diff --git a/packaging/musl/README.md b/packaging/musl/README.md new file mode 100644 index 00000000..b28cf023 --- /dev/null +++ b/packaging/musl/README.md @@ -0,0 +1,35 @@ +# Portable Linux server build + +This build produces one `x86_64` Linux server executable with embedded +PostgreSQL. The Rust program is statically linked against musl. PostgreSQL is +compiled against musl without optional host-library dependencies and is stored +inside the executable by `postgresql_embedded`. + +PostgreSQL itself remains dynamically loadable so the `btree_gist` extension +used by the migrations works. Its musl loader is included in the embedded +archive, and static launcher executables invoke every PostgreSQL tool through +that loader. Neither glibc, musl, PostgreSQL nor timezone data needs to be +installed on the target machine. + +Build directly through the repository's Nix flake: + +```sh +nix build '.?submodules=1#portable-server' +``` + +The `submodules=1` flag includes the workspace's nested Rust repositories. The +artifact is available as `result/bin/server`. The derivation initializes a +temporary cluster, creates `btree_gist`, sets the timezone to `UTC`, and checks +that the final Rust executable has no ELF interpreter. + +The resulting executable is intended for 64-bit Linux kernels. Run it normally: + +```sh +./result/bin/server server +``` + +Use the external PostgreSQL build on NixOS when preferred: + +```sh +cargo build --release --package server --no-default-features +``` diff --git a/packaging/musl/default.nix b/packaging/musl/default.nix new file mode 100644 index 00000000..4938af1b --- /dev/null +++ b/packaging/musl/default.nix @@ -0,0 +1,171 @@ +{ pkgs, source, version }: + +let + muslPkgs = pkgs.pkgsMusl; + postgresqlVersion = "17.10"; + postgresqlArchiveVersion = "17.10.0"; + archiveName = "postgresql-${postgresqlArchiveVersion}-x86_64-unknown-linux-musl.tar.gz"; + + bootstrapInput = pname: inputs: + pkgs.lib.findFirst + (input: (input.pname or "") == pname) + (throw "Nixpkgs does not expose ${pname} for the musl toolchain") + inputs; + bootstrapRustPlatform = muslPkgs.makeRustPlatform { + rustc = bootstrapInput + "rustc-bootstrap" + muslPkgs.rustc-unwrapped.nativeBuildInputs; + cargo = bootstrapInput + "cargo-bootstrap" + muslPkgs.cargo.nativeBuildInputs; + }; + + staticOpenSSL = muslPkgs.openssl.overrideAttrs (old: { + configureFlags = + builtins.filter (flag: flag != "shared") old.configureFlags + ++ [ "no-shared" ]; + doCheck = false; + }); + + postgresqlSource = pkgs.fetchurl { + url = "https://ftp.postgresql.org/pub/source/v${postgresqlVersion}/postgresql-${postgresqlVersion}.tar.bz2"; + hash = "sha256-B4oDUW3NvbcF/sr0Feo9E6lWxYnkbwn+1ooG+wBZjJA="; + }; + + postgresqlArchive = muslPkgs.stdenv.mkDerivation { + pname = "komp-ac-postgresql-musl"; + version = postgresqlArchiveVersion; + src = postgresqlSource; + + nativeBuildInputs = [ + pkgs.bison + pkgs.file + pkgs.flex + pkgs.gnutar + pkgs.gzip + pkgs.perl + ]; + + configurePhase = '' + runHook preConfigure + ./configure \ + --prefix="$out/postgresql" \ + --disable-nls \ + --without-gssapi \ + --without-icu \ + --without-ldap \ + --without-libxml \ + --without-libxslt \ + --without-llvm \ + --without-lz4 \ + --without-openssl \ + --without-pam \ + --without-readline \ + --without-systemd \ + --without-zlib \ + --without-zstd + runHook postConfigure + ''; + + buildPhase = '' + runHook preBuild + make -j"$NIX_BUILD_CORES" + make -C contrib/btree_gist -j"$NIX_BUILD_CORES" + runHook postBuild + ''; + + installPhase = '' + runHook preInstall + make install-strip + make -C contrib/btree_gist install + + "$CC" -static -Os -s -Wall -Wextra -Werror \ + -o postgresql-launcher \ + ${./postgresql-launcher.c} + + mkdir -p "$out/postgresql/libexec" + for tool in "$out"/postgresql/bin/*; do + toolName="''${tool##*/}" + mv "$tool" "$out/postgresql/libexec/$toolName" + cp postgresql-launcher "$out/postgresql/bin/$toolName" + done + cp ${muslPkgs.musl}/lib/ld-musl-x86_64.so.1 \ + "$out/postgresql/lib/ld-musl-x86_64.so.1" + + file "$out/postgresql/bin/postgres" | grep -q 'statically linked' + "$out/postgresql/bin/postgres" --version + + mkdir postgresql-data postgresql-socket + "$out/postgresql/bin/initdb" \ + --auth=trust \ + --no-locale \ + -D "$PWD/postgresql-data" + "$out/postgresql/bin/pg_ctl" \ + -D "$PWD/postgresql-data" \ + -l "$PWD/postgresql.log" \ + -o "-k $PWD/postgresql-socket -p 55432" \ + -w start + "$out/postgresql/bin/psql" \ + -h "$PWD/postgresql-socket" \ + -p 55432 \ + -d postgres \ + -c "CREATE EXTENSION btree_gist; SET TIME ZONE 'UTC';" + "$out/postgresql/bin/pg_ctl" \ + -D "$PWD/postgresql-data" \ + -m fast \ + -w stop + + tar -C "$out" -czf "$out/${archiveName}" postgresql + runHook postInstall + ''; + + dontPatchELF = true; + dontStrip = true; + }; +in +bootstrapRustPlatform.buildRustPackage { + pname = "komp-ac-server-portable"; + inherit version source; + src = source; + + cargoLock.lockFile = ../../Cargo.lock; + cargoBuildFlags = [ + "--package" + "server" + "--no-default-features" + "--features" + "portable-postgres" + ]; + auditable = false; + doCheck = false; + + nativeBuildInputs = [ + pkgs.binutils + pkgs.cmake + pkgs.file + pkgs.pkg-config + pkgs.protobuf + ]; + buildInputs = [ staticOpenSSL ]; + + OPENSSL_STATIC = "1"; + OPENSSL_LIB_DIR = "${staticOpenSSL.out}/lib"; + OPENSSL_INCLUDE_DIR = "${staticOpenSSL.dev}/include"; + POSTGRESQL_VERSION = "=${postgresqlArchiveVersion}"; + KOMP_AC_POSTGRESQL_ARCHIVE = "${postgresqlArchive}/${archiveName}"; + KOMP_AC_POSTGRESQL_VERSION = postgresqlArchiveVersion; + SQLX_OFFLINE = "true"; + RUSTFLAGS = "-Ctarget-feature=+crt-static"; + + postInstall = '' + file "$out/bin/server" | grep -q 'statically linked' + ! readelf -l "$out/bin/server" | grep -q 'Requesting program interpreter' + ''; + + meta = { + description = "Portable komp_ac server with embedded musl PostgreSQL"; + license = pkgs.lib.licenses.agpl3Plus; + platforms = [ "x86_64-linux" ]; + mainProgram = "server"; + }; +} diff --git a/packaging/musl/postgresql-launcher.c b/packaging/musl/postgresql-launcher.c new file mode 100644 index 00000000..90ec6a82 --- /dev/null +++ b/packaging/musl/postgresql-launcher.c @@ -0,0 +1,92 @@ +#define _GNU_SOURCE + +#include +#include +#include +#include +#include +#include + +static void fail(const char *message) +{ + fprintf(stderr, "embedded PostgreSQL launcher: %s\n", message); + exit(127); +} + +static void join_path(char *destination, size_t capacity, const char *left, const char *right) +{ + int length = snprintf(destination, capacity, "%s/%s", left, right); + if (length < 0 || (size_t) length >= capacity) + fail("installation path is too long"); +} + +int main(int argc, char **argv) +{ + char executable[PATH_MAX]; + ssize_t executable_length = readlink("/proc/self/exe", executable, sizeof(executable) - 1); + if (executable_length < 0) { + perror("embedded PostgreSQL launcher: /proc/self/exe"); + return 127; + } + executable[executable_length] = '\0'; + + char *tool = strrchr(executable, '/'); + if (tool == NULL || tool[1] == '\0') + fail("cannot determine the requested PostgreSQL tool"); + tool++; + + char root[PATH_MAX]; + if ((size_t) executable_length >= sizeof(root)) + fail("installation path is too long"); + memcpy(root, executable, (size_t) executable_length + 1); + + char *bin_separator = strrchr(root, '/'); + if (bin_separator == NULL) + fail("cannot determine the PostgreSQL installation directory"); + *bin_separator = '\0'; + char *root_separator = strrchr(root, '/'); + if (root_separator == NULL) + fail("cannot determine the PostgreSQL installation directory"); + *root_separator = '\0'; + + char loader[PATH_MAX]; + char library_path[PATH_MAX]; + char real_tool[PATH_MAX]; + join_path(loader, sizeof(loader), root, "lib/ld-musl-x86_64.so.1"); + join_path(library_path, sizeof(library_path), root, "lib"); + + char real_tool_relative[PATH_MAX]; + int relative_length = snprintf( + real_tool_relative, + sizeof(real_tool_relative), + "libexec/%s", + tool + ); + if (relative_length < 0 || (size_t) relative_length >= sizeof(real_tool_relative)) + fail("tool path is too long"); + join_path(real_tool, sizeof(real_tool), root, real_tool_relative); + + char **loader_argv = calloc((size_t) argc + 6, sizeof(char *)); + if (loader_argv == NULL) { + perror("embedded PostgreSQL launcher: calloc"); + return 127; + } + + loader_argv[0] = loader; + loader_argv[1] = "--library-path"; + loader_argv[2] = library_path; + loader_argv[3] = "--argv0"; + loader_argv[4] = executable; + loader_argv[5] = real_tool; + for (int index = 1; index < argc; index++) + loader_argv[index + 5] = argv[index]; + + execv(loader, loader_argv); + fprintf( + stderr, + "embedded PostgreSQL launcher: cannot execute %s: %s\n", + real_tool, + strerror(errno) + ); + return 127; +} diff --git a/server b/server index b79e9a0b..440e0cf0 160000 --- a/server +++ b/server @@ -1 +1 @@ -Subproject commit b79e9a0b57a159b7c2bf13db737a77b41f1c213a +Subproject commit 440e0cf0dc0f9190827c5b62657a07f586afbdf5