From c9be2a5a1aa9313369bcefaed27eee37da7449d1 Mon Sep 17 00:00:00 2001 From: xenia Date: Sun, 28 Sep 2025 20:24:40 -0400 Subject: [PATCH] implement racket packaging and environments --- README.md | 62 ++++++++++++++++ build-racket-package.nix | 54 ++++++++++++++ flake.nix | 84 ++++++++++++++++++++- make-racket-env.nix | 15 ++++ racket-install-hook.nix | 7 ++ racket-install-hook.sh | 153 +++++++++++++++++++++++++++++++++++++++ 6 files changed, 372 insertions(+), 3 deletions(-) create mode 100644 README.md create mode 100644 build-racket-package.nix create mode 100644 make-racket-env.nix create mode 100644 racket-install-hook.nix create mode 100644 racket-install-hook.sh diff --git a/README.md b/README.md new file mode 100644 index 0000000..7f5ad9b --- /dev/null +++ b/README.md @@ -0,0 +1,62 @@ +# nix support for racket + +implements nix helpers for building racket packages and environments + +this is a complete new implementation of this functionality. i did not use racket2nix because +racket2nix is messy and hasn't been updated for a while. this is a significantly simpler +implementation owning to better use of stdenv utilities and racket features + +## how to use + +### make nix packages for racket packages + +```nix +pkgs.racketPackages.callPackage ( + { + lib, + buildRacketPackage, + fetchFromGitHub, + }: buildRacketPackage { + pname = "ansi-color"; + version = "0.2"; + + src = final.fetchFromGitHub { + owner = "renatoathaydes"; + repo = "ansi-color"; + rev = "0.2"; + hash = "sha256-7WDW+4R9K+XLb9nMNGQlU+zAi2Gq7cUqzO3csN+AJvI="; + }; + } +) {} + +``` + +### build a racket environment + +unfortunately, due to racket limitations, `nix-shell`/`nix develop` will not work at all for racket +packages that are created using this nix code. instead, similar to `python.buildEnv` / +`python.withPackages`, you can create a racket environment with a given set of packages in it. this +functions somewhat like a python "virtualenv" for racket. it is implemented by creating a "tethered +installation" -- see . + +```nix +with pkgs.racketPackages; makeRacketEnv { + packages = [ + ansi-color + ]; +}; + +``` + +the resulting environment will have a `bin/racket` executable which has the declared packages +available + +## future work + +- automatically convert info.rkt into packaging. this should be doable for most of the catalog that + is currently building on the CI, and perhaps some additional steps are needed for native library + dependencies declared in info.rkt +- allow using either racket or racket-minimal as a base (currently only racket non-minimal is + supported) +- instead of having 2 separate packages, rework racket to be a layered installation on top of + racket-minimal, reducing duplication. this is similar to how guix does it diff --git a/build-racket-package.nix b/build-racket-package.nix new file mode 100644 index 0000000..9b3f637 --- /dev/null +++ b/build-racket-package.nix @@ -0,0 +1,54 @@ +{ + lib, + racket, + racketInstallHook, + stdenv, + + wrapGAppsHook3, +}: lib.extendMkDerivation { + constructDrv = stdenv.mkDerivation; + excludeDrvArgNames = [ + "dependencies" + ]; + extendDrvArgs = finalAttrs: + { + pname, + version, + nativeBuildInputs ? [], + propagatedBuildInputs ? [], + + dependencies ? [], + + tetheredInstallation ? false, + ... + } @ attrs: { + name = "racket${racket.version}-" + pname + "-" + version; + + strictDeps = true; + + dontConfigure = true; + dontBuild = true; + + racketTetheredInstallation = tetheredInstallation; + + nativeBuildInputs = [ + racket + racketInstallHook + + wrapGAppsHook3 + ] ++ nativeBuildInputs; + + propagatedBuildInputs = [racket] ++ dependencies ++ propagatedBuildInputs; + + dontWrapGApps = true; + + preFixup = '' + find $out/bin -type f -executable -print0 | + while IFS= read -r -d ''' f; do + if test "$(file --brief --mime-type "$f")" = application/x-executable; then + wrapGApp "$f" + fi + done + ''; + }; +} diff --git a/flake.nix b/flake.nix index b4bb50b..e04c7a7 100644 --- a/flake.nix +++ b/flake.nix @@ -1,7 +1,85 @@ { - description = "A very basic flake"; + description = "Flake for racket packaging test"; - outputs = { self, dragnpkgs } @ inputs: dragnpkgs.lib.mkFlake { - packages.racket = ./racket/package.nix; + outputs = { self, dragnpkgs } @ inputs: { + legacyPackages.x86_64-linux = dragnpkgs.lib.dragnpkgs-custom { + system = "x86_64-linux"; + overlays = [self.overlays.default]; + }; + + overlays.default = final: prev: { + racket-minimal = final.callPackage ./racket/minimal.nix {}; + racket = final.callPackage ./racket/package.nix {}; + + racketPackages = final.lib.makeScope final.newScope (self: { + racketInstallHook = self.callPackage ./racket-install-hook.nix {}; + buildRacketPackage = self.callPackage ./build-racket-package.nix {}; + + makeRacketEnv = self.callPackage ./make-racket-env.nix {}; + + testEnv = self.makeRacketEnv { + packages = [self.ansi-color]; + }; + + ansi-color = self.callPackage ( + { + lib, + buildRacketPackage, + fetchFromGitHub, + }: buildRacketPackage { + pname = "ansi-color"; + version = "0.2"; + + src = final.fetchFromGitHub { + owner = "renatoathaydes"; + repo = "ansi-color"; + rev = "0.2"; + hash = "sha256-7WDW+4R9K+XLb9nMNGQlU+zAi2Gq7cUqzO3csN+AJvI="; + }; + } + ) {}; + + namespaced-transformer-lib = self.callPackage ( + { + lib, + buildRacketPackage, + fetchFromGitHub, + }: buildRacketPackage { + pname = "namespaced-transformer-lib"; + version = "none"; + + src = "${final.fetchFromGitHub { + owner = "lexi-lambda"; + repo = "namespaced-transformer"; + rev = "4cdc1bdae09a07b78f23665267f2c7df4be5a7f6"; + hash = "sha256-vpDJ7qUNhKreig1LbU33d7TXXHkfd7gPxE1Fc5bvzFE="; + }}/namespaced-transformer-lib"; + } + ) {}; + + curly-fn-lib = self.callPackage ( + { + lib, + buildRacketPackage, + fetchFromGitHub, + + namespaced-transformer-lib, + }: buildRacketPackage { + pname = "curly-fn-lib"; + version = "none"; + + dependencies = [ namespaced-transformer-lib ]; + + src = "${final.fetchFromGitHub { + owner = "lexi-lambda"; + repo = "racket-curly-fn"; + rev = "d64cd71d5b386be85f5979edae6f6b6469a4df86"; + hash = "sha256-ge7o/UAvUXA4DIl03UkqVnLHNaPGq4SqxWvpcKdXndI="; + }}/curly-fn-lib"; + } + ) {}; + + }); + }; }; } diff --git a/make-racket-env.nix b/make-racket-env.nix new file mode 100644 index 0000000..51ac4ab --- /dev/null +++ b/make-racket-env.nix @@ -0,0 +1,15 @@ +{ + lib, + racket, + buildRacketPackage, +}: { + packages, +}: buildRacketPackage { + pname = "env"; + version = "0"; + + unpackPhase = "touch nix-racket-env-only"; + + dependencies = packages; + tetheredInstallation = true; +} diff --git a/racket-install-hook.nix b/racket-install-hook.nix new file mode 100644 index 0000000..78198d8 --- /dev/null +++ b/racket-install-hook.nix @@ -0,0 +1,7 @@ +{ + racket, + makeSetupHook, +}: makeSetupHook { + name = "racket-install-hook"; + propagatedBuildInputs = [ racket ]; +} ./racket-install-hook.sh diff --git a/racket-install-hook.sh b/racket-install-hook.sh new file mode 100644 index 0000000..3f25b32 --- /dev/null +++ b/racket-install-hook.sh @@ -0,0 +1,153 @@ +echo "Sourcing racket-install-hook" + +addRacketPath() { + if [ -f "$1/nix-support/racket-pkg" ]; then + addToSearchPathWithCustomDelimiter : NIX_RACKET_PKG_PATH $1 + fi +} + +racketInstallPhase() { + echo "Executing racketInstallPhase" + runHook preInstall + + mkdir -p $out/{include,etc/racket,lib/racket,share/racket/pkgs,share/racket/collects,bin,share/applications,share/doc/racket,share/man} + + mkdir -p $out/nix-support + touch $out/nix-support/racket-pkg + + out="$out" tethered="$racketTetheredInstallation" \ + racket --no-user-path -nl racket/base -f - <path-list (or (getenv "NIX_RACKET_PKG_PATH") "") '())) + +(define tethered? (equal? (getenv "tethered") "1")) + +(define base-config (read-installation-configuration-table)) + +(define (add-to-search added-list search-list) + (match search-list + ['() (error "no #f found in search list!")] + [(cons #f rst) (cons #f (append added-list rst))] + [(cons fst rst) (cons fst (add-to-search added-list rst))])) + +(define (make-search-path* key list-key [pkgs-search '()]) + (define old-search-list (hash-ref base-config list-key '(#f))) + (define old-value + (cond + [(hash-has-key? base-config key) + (list (hash-ref base-config key))] + [(eq? key 'links-file) + (list + (path->string + (build-path (hash-ref base-config 'share-dir) "links.rktd")))] + [else (error "no key" key)])) + (define added-list (append pkgs-search old-value)) + (add-to-search added-list old-search-list)) + +(define (default-location pkg key) + (path->string + (match key + ['include-dir (build-path pkg "include")] + ['lib-dir (build-path pkg "lib/racket")] + ['share-dir (build-path pkg "share/racket")] + ['pkgs-dir (build-path pkg "share/racket/pkgs")] + ['links-file (build-path pkg "share/racket/links.rktd")] + ['bin-dir (build-path pkg "bin")] + ['doc-dir (build-path pkg "share/doc/racket")] + ['man-dir (build-path pkg "share/man")] + [_ (error "unexpected key:" key)]))) + +(define (make-search-path key list-key) + (define pkgs-search + (for/list ([pkg (in-list pkgs)]) + (default-location pkg key))) + + (make-search-path* key list-key pkgs-search)) + +(define (add-libs lib-path) + (define ldflags (string-split (getenv "NIX_LDFLAGS"))) + (define libs + (for/list ([lib (in-list ldflags)] #:when (string-prefix? "-L" lib)) + (string-trim "-L" #:right? #f))) + (remove-duplicates (append libs lib-path))) + +(define config* + (hash + 'absolute-installation? #t + 'build-stamp "" + 'catalogs (hash-ref base-config 'catalogs) + 'compiled-file-roots (hash-ref base-config 'compiled-file-roots) + + 'apps-dir (path->string (build-path out "share/applications")) + + 'bin-dir (default-location out 'bin-dir) + 'bin-search-dirs (make-search-path 'bin-dir 'bin-search-dirs) + + 'doc-dir (default-location out 'doc-dir) + 'doc-search-dirs (make-search-path 'doc-dir 'doc-search-dirs) + 'doc-search-url (hash-ref base-config 'doc-search-url) + + 'include-dir (default-location out 'include-dir) + 'include-search-dirs (make-search-path 'include-dir 'include-search-dirs) + + 'lib-dir (default-location out 'lib-dir) + 'lib-search-dirs (add-libs (make-search-path 'lib-dir 'lib-search-dirs)) + + 'links-file (default-location out 'links-file) + 'links-search-files (make-search-path 'links-file 'links-search-files) + + 'man-dir (default-location out 'man-dir) + 'man-search-dirs (make-search-path 'man-dir 'man-search-dirs) + + 'pkgs-dir (default-location out 'pkgs-dir) + 'pkgs-search-dirs (make-search-path 'pkgs-dir 'pkgs-search-dirs) + + 'share-dir (default-location out 'share-dir) + 'share-search-dirs (make-search-path 'share-dir 'share-search-dirs))) + +(define config + (if tethered? + (hash-union + config* + (hash + 'config-tethered-console-bin-dir (hash-ref config* 'bin-dir) + 'config-tethered-gui-bin-dir (hash-ref config* 'bin-dir) + 'config-tethered-apps-dir (hash-ref config* 'apps-dir))) + config*)) + +(with-output-to-file (build-path out "etc/racket/config.rktd") + (curry pretty-write config)) +EOF + + echo Initializing installation layer + if [ "$racketTetheredInstallation" == "true" ]; then + racket --config $out/etc/racket/ --no-user-path -l- \ + raco setup + else + racket --config $out/etc/racket/ --no-user-path -l- \ + raco setup --no-launcher + + rm $out/bin/mzscheme # ???? + fi + + if [ -f "nix-racket-env-only" ]; then + echo Skipping raco pkg install + else + echo Running raco pkg install + racket --config $out/etc/racket/ --no-user-path -l- \ + raco pkg install --installation --deps fail --copy --name "$pname" "$(readlink -e .)" + fi + + runHook postInstall + echo "Finished executing racketInstallPhase" +} + + +if [ -z "${dontUseRacketInstall-}" ] && [ -z "${installPhase-}" ]; then + echo "Adding racket env hook" + addEnvHooks "$targetOffset" addRacketPath + echo "Using racketInstallPhase" + installPhase=racketInstallPhase +fi