(define-module (tribes packages source) #:use-module ((guix licenses) #:prefix license:) #:use-module (guix download) #:use-module (guix git-download) #:use-module (guix gexp) #:use-module (guix packages) #:use-module (guix utils) #:use-module (gnu packages autotools) #:use-module (gnu packages base) #:use-module (gnu packages commencement) #:use-module (gnu packages gawk) #:use-module (gnu packages linux) #:use-module (gnu packages m4) #:use-module (gnu packages node) #:use-module (gnu packages perl) #:use-module (gnu packages pkg-config) #:use-module ((tribes packages mix) #:prefix mix:) #:use-module (srfi srfi-1) #:use-module (srfi srfi-13) #:export (fetch-mix-deps fetch-npm-deps local-tribes-package tribes-package tribes-source-package tribes-source-directory->local-file)) ;; Recursive sha256 of the raw deps tree produced by `mix deps.get --only prod` ;; from the current Tribes mix.lock, with git metadata stripped except for ;; .git/HEAD in SCM dependencies. (define %tribes-raw-mix-deps-sha256 "0mv4jva8zkx8cq1b84hn65bl913nnhkvf25g6fi93z3jm35jy0pc") ;; Recursive sha256 of the Tribes-specific prepared deps tree, after injecting ;; the upstream secp256k1 source into the Hex package and patching its build ;; recipe to avoid build-time network access. (define %tribes-mix-deps-sha256 "0ksjnc9gnjijp1nbz3jlvl9kz8w7hx1a0ssms1dvd15rr25gn0d4") ;; Recursive sha256 of assets/node_modules generated from assets/package-lock.json ;; in an isolated build environment, with local file dependencies resolved from ;; the vendored Mix dependency tree. (define %tribes-npm-deps-sha256 "1bfzs67ffhwcm0dwdkb1jqnbn3fpgj22zfhd2y907w8daj62gahv") (define %tribes-home-page "https://git.teralink.net/tribes/tribes.git") (define %tribes-commit "1e026a60fdabcec8b9ec0a850a03f903d02496e7") (define %tribes-revision "1") (define %tribes-version (git-version "0.2.0" %tribes-revision %tribes-commit)) (define %tribes-source-sha256 "1lqls1ngy3vpf0hh9jn44h1g9rx1hglj9ik3zi1ywcr4q01pmv68") (define %tribes-upstream-source (origin (method git-fetch) (uri (git-reference (url %tribes-home-page) (commit %tribes-commit))) (file-name (git-file-name "tribes" %tribes-version)) (sha256 (base32 %tribes-source-sha256)))) (define %libsecp256k1-upstream-source (origin (method git-fetch) (uri (git-reference (url "https://github.com/bitcoin-core/secp256k1") (commit "v0.7.1"))) (file-name (git-file-name "secp256k1" "0.7.1")) (sha256 (base32 "10cvh8jks3rjg6p7y0vm1v4kw9y7vljbfijj0zxwkxzysxx60w0f")))) (define %heroicons-upstream-source (origin (method git-fetch) (uri (git-reference (url "https://github.com/tailwindlabs/heroicons") (commit "0435d4ca364a608cc75e2f8683d374e55abbae26"))) (file-name (git-file-name "heroicons" "2.2.0")) (sha256 (base32 "15di4p755ydivkbv9mv9hb8lsdgzb5zq77ljdnyp76cvykanpk15")))) (define %excluded-root-basenames '(".cache" ".claude" ".direnv" ".elixir_ls" ".git" ".hex" ".home" ".mypy_cache" ".mix-home" ".pre-commit-config.yaml" "_build" "deps" "erl_crash.dump" "node_modules" "result")) (define (root-artifact-path? root file entry) (let ((entry-path (string-append root "/" entry))) (or (string=? file entry-path) (string-prefix? (string-append entry-path "/") file)))) (define (transient-source-file? root file) (let ((base (basename file))) (or (any (lambda (entry) (root-artifact-path? root file entry)) %excluded-root-basenames) (root-artifact-path? root file ".env") (string-prefix? ".devenv" base) (string=? base ".env") (string-suffix? ".log" base) (string-suffix? ".tsbuildinfo" base) (string-suffix? ".db" base) (string-suffix? ".db-shm" base) (string-suffix? ".db-wal" base)))) (define (tribes-source-select? root file stat) (or (string=? file root) (not (transient-source-file? root file)))) (define (tribes-source-directory->local-file directory) "Return DIRECTORY as a recursively copied local-file, excluding transient build artifacts and, when available, anything not tracked in the enclosing Git checkout." (let ((git-select? (git-predicate directory))) (local-file directory #:recursive? #t #:select? (if git-select? (lambda (file stat) (and (git-select? file stat) (tribes-source-select? directory file stat))) (lambda (file stat) (tribes-source-select? directory file stat)))))) (define fetch-mix-deps mix:fetch-mix-deps) (define* (fetch-npm-deps source #:key mix-fod-deps (name "tribes-npm-deps") (version "0.2.0") (sha256 %tribes-npm-deps-sha256)) "Return a fixed-output node_modules tree for SOURCE/assets/package-lock.json, with local file dependencies resolved from MIX-FOD-DEPS." (computed-file (string-append name "-" version) (with-imported-modules '((guix build utils)) #~(begin (use-modules (guix build utils)) (define out #$output) (define work (string-append (getcwd) "/build")) (define app-dir (string-append work "/app")) (define deps-dir (string-append app-dir "/deps")) (define assets-dir (string-append app-dir "/assets")) (define certs-dir #$(file-append nss-certs "/etc/ssl/certs")) (define cert-file (string-append work "/ca-certificates.crt")) (define path (string-join (list #$(file-append node "/bin") #$(file-append bash-minimal "/bin") #$(file-append coreutils "/bin") #$(file-append findutils "/bin") #$(file-append git-minimal "/bin") #$(file-append gzip "/bin") #$(file-append tar "/bin") (or (getenv "PATH") "")) ":")) (mkdir-p work) (copy-recursively #+source app-dir #:follow-symlinks? #t) (invoke #$(file-append coreutils "/bin/chmod") "-R" "u+w" app-dir) (when (file-exists? deps-dir) (delete-file-recursively deps-dir)) (copy-recursively #+mix-fod-deps deps-dir #:follow-symlinks? #t) (invoke #$(file-append coreutils "/bin/chmod") "-R" "u+w" deps-dir) (invoke #$(file-append bash-minimal "/bin/sh") "-c" (string-append #$(file-append coreutils "/bin/cat") " " certs-dir "/*.pem > " cert-file)) (setenv "PATH" path) (setenv "HOME" (string-append work "/home")) (setenv "XDG_CACHE_HOME" (string-append work "/cache")) (setenv "npm_config_cache" (string-append work "/npm-cache")) (setenv "npm_config_userconfig" (string-append work "/npmrc")) (setenv "SSL_CERT_DIR" certs-dir) (setenv "SSL_CERT_FILE" cert-file) (setenv "NODE_ENV" "production") (mkdir-p (getenv "HOME")) (mkdir-p (getenv "XDG_CACHE_HOME")) (mkdir-p (getenv "npm_config_cache")) (with-directory-excursion assets-dir (invoke "npm" "ci" "--ignore-scripts" "--no-audit" "--no-fund")) (mkdir-p out) (copy-recursively (string-append assets-dir "/node_modules") out #:follow-symlinks? #t))) #:options `(#:hash ,(base32 sha256) #:hash-algo sha256 #:recursive? #t #:leaked-env-vars ("http_proxy" "https_proxy" "LC_ALL" "LC_MESSAGES" "LANG" "COLUMNS")))) (define* (tribes-mix-deps source #:key (name "tribes-mix-deps") (version "0.2.0") (sha256 %tribes-mix-deps-sha256) (raw-sha256 %tribes-raw-mix-deps-sha256) (mix-env "prod") (mix-target "host")) "Return the Tribes Mix dependency tree, prepared from the raw lockfile resolution by injecting extra pre-fetched sources needed for offline builds." (let ((raw-mix-deps (fetch-mix-deps source #:name (string-append name "-raw") #:version version #:sha256 raw-sha256 #:mix-env mix-env #:mix-target mix-target))) (computed-file (string-append name "-" version) (with-imported-modules '((guix build utils)) #~(begin (use-modules (guix build utils)) (define out #$output) (define deps-dir (string-append (getcwd) "/deps")) (copy-recursively #+raw-mix-deps deps-dir #:follow-symlinks? #t) (invoke #$(file-append coreutils "/bin/chmod") "-R" "u+w" deps-dir) (let* ((libsecp-dir (string-append deps-dir "/lib_secp256k1")) (libsecp-src-dir (string-append libsecp-dir "/c_src/secp256k1")) (libsecp-makefile (string-append libsecp-dir "/Makefile")) (libsecp-fetched-stamp (string-append libsecp-src-dir "/.fetched"))) (when (file-exists? libsecp-dir) (mkdir-p (string-append libsecp-dir "/c_src")) (when (file-exists? libsecp-src-dir) (delete-file-recursively libsecp-src-dir)) (copy-recursively #+%libsecp256k1-upstream-source libsecp-src-dir #:follow-symlinks? #t) (invoke #$(file-append coreutils "/bin/chmod") "-R" "u+w" libsecp-dir) (when (file-exists? libsecp-makefile) ;; Avoid depending on preserved executable bits or /bin/sh in the ;; generated Autoconf script. (substitute* libsecp-makefile (("\\./autogen\\.sh") "sh ./autogen.sh") (("\\./configure") "sh ./configure"))) (call-with-output-file libsecp-fetched-stamp (lambda (_port) #t)))) (mkdir-p out) (copy-recursively deps-dir out #:follow-symlinks? #t))) #:options `(#:hash ,(base32 sha256) #:hash-algo sha256 #:recursive? #t #:leaked-env-vars ("http_proxy" "https_proxy" "LC_ALL" "LC_MESSAGES" "LANG" "COLUMNS"))))) (define* (tribes-source-package source #:key (mix-deps #f) (mix-deps-sha256 %tribes-mix-deps-sha256) (raw-mix-deps-sha256 %tribes-raw-mix-deps-sha256) (npm-deps #f) (npm-deps-sha256 %tribes-npm-deps-sha256) (name "tribes") (version %tribes-version) (home-page %tribes-home-page) (synopsis "Tribes social app") (description "Tribes social application built from source as a production Elixir release using vendored Mix and npm dependency trees.")) "Return a Guix package that builds a production Tribes release from SOURCE, using MIX-DEPS and NPM-DEPS as pre-fetched dependency trees resolved from mix.lock and assets/package-lock.json." (let* ((mix-deps-source (or mix-deps (tribes-mix-deps source #:name (string-append name "-mix-deps") #:version version #:sha256 mix-deps-sha256 #:raw-sha256 raw-mix-deps-sha256))) (npm-deps-source (or npm-deps (fetch-npm-deps source #:mix-fod-deps mix-deps-source #:name (string-append name "-npm-deps") #:version version #:sha256 npm-deps-sha256)))) (mix:mix-release-package source #:mix-fod-deps mix-deps-source #:name name #:version version #:home-page home-page #:synopsis synopsis #:description description #:license license:asl2.0 #:native-inputs (list autoconf autoconf-wrapper automake gcc-toolchain gawk grep gnu-make libtool linux-libre-headers m4 node perl pkg-config sed) #:path-inputs (list autoconf autoconf-wrapper automake gcc-toolchain gawk grep gnu-make libtool m4 node perl pkg-config sed) #:aclocal-inputs (list automake libtool) #:setup-gexp #~(begin (define kernel-headers-dir #$(file-append linux-libre-headers "/include")) (let ((existing-cpath (getenv "CPATH"))) (setenv "CPATH" (if existing-cpath (string-append kernel-headers-dir ":" existing-cpath) kernel-headers-dir))) (let ((existing-c-include-path (getenv "C_INCLUDE_PATH"))) (setenv "C_INCLUDE_PATH" (if existing-c-include-path (string-append kernel-headers-dir ":" existing-c-include-path) kernel-headers-dir))) (setenv "CC" #$(file-append gcc-toolchain "/bin/gcc")) (setenv "CXX" #$(file-append gcc-toolchain "/bin/g++")) (setenv "CPP" (string-append #$(file-append gcc-toolchain "/bin/gcc") " -E"))) #:configure-gexp #~(begin ;; Absinthe has been the last fragile dependency in Guix builds so far. ;; Compile its immediate deps first, then compile Absinthe in its own ;; pass before compiling the remaining dependency graph. (invoke "mix" "deps.compile" "nimble_parsec" "telemetry" "decimal") (let ((existing-erl-flags (getenv "ERL_FLAGS"))) ;; Keep the scheduler count pinned while compiling Absinthe so ;; toolchain-sensitive ordering issues stay deterministic. (setenv "ERL_FLAGS" (if existing-erl-flags (string-append existing-erl-flags " +S 1:1") "+S 1:1")) (invoke "mix" "deps.compile" "absinthe" "--force") (if existing-erl-flags (setenv "ERL_FLAGS" existing-erl-flags) (unsetenv "ERL_FLAGS"))) (invoke "mix" "deps.compile")) #:build-gexp #~(begin (invoke "mix" "compile" "--no-deps-check") (let ((assets-node-modules "assets/node_modules")) (when (file-exists? assets-node-modules) (delete-file-recursively assets-node-modules)) (copy-recursively #+npm-deps-source assets-node-modules #:follow-symlinks? #t) (invoke #$(file-append coreutils "/bin/chmod") "-R" "u+w" assets-node-modules) (invoke "find" assets-node-modules "-type" "f" "-path" "*/@esbuild/*/bin/esbuild" "-exec" "chmod" "+x" "{}" "+") (invoke "find" assets-node-modules "-type" "f" "-path" "*/.bin/*" "-exec" "chmod" "+x" "{}" "+")) (let ((heroicons-dir "deps/heroicons")) (when (file-exists? heroicons-dir) (delete-file-recursively heroicons-dir)) (copy-recursively #+%heroicons-upstream-source heroicons-dir #:follow-symlinks? #t) (invoke #$(file-append coreutils "/bin/chmod") "-R" "u+w" heroicons-dir)) (setenv "NODE_PATH" (string-append (getcwd) "/deps:" (getcwd) "/_build/prod")) (with-directory-excursion "assets" (invoke "npm" "run" "build.css" "--" "--minify") (invoke "npm" "run" "build.js" "--" "--minify")) (invoke "mix" "phx.digest")) #:install-gexp #~(begin (invoke "mix" "release" "--no-deps-check" "--path" out) (let ((launcher (string-append out "/bin/" #$name)) (launcher-app (string-append out "/bin/" #$name "-app"))) (when (file-exists? launcher) (rename-file launcher launcher-app))))))) (define* (local-tribes-package directory #:key (version "dev") (mix-deps-sha256 #f) (raw-mix-deps-sha256 #f) (npm-deps-sha256 #f)) "Return a Tribes package built from a local source checkout. Hash overrides allow development against changed lockfiles without changing the canonical package pin." (tribes-source-package (tribes-source-directory->local-file directory) #:version version #:mix-deps-sha256 (or mix-deps-sha256 %tribes-mix-deps-sha256) #:raw-mix-deps-sha256 (or raw-mix-deps-sha256 %tribes-raw-mix-deps-sha256) #:npm-deps-sha256 (or npm-deps-sha256 %tribes-npm-deps-sha256))) (define tribes-package (tribes-source-package %tribes-upstream-source #:version %tribes-version))