implement racket packaging and environments

This commit is contained in:
xenia 2025-09-28 20:24:40 -04:00
parent fc5b593038
commit c9be2a5a1a
6 changed files with 372 additions and 3 deletions

62
README.md Normal file
View File

@ -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 <https://docs.racket-lang.org/raco/tethered-install.html>.
```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

54
build-racket-package.nix Normal file
View File

@ -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
'';
};
}

View File

@ -1,7 +1,85 @@
{ {
description = "A very basic flake"; description = "Flake for racket packaging test";
outputs = { self, dragnpkgs } @ inputs: dragnpkgs.lib.mkFlake { outputs = { self, dragnpkgs } @ inputs: {
packages.racket = ./racket/package.nix; 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";
}
) {};
});
};
}; };
} }

15
make-racket-env.nix Normal file
View File

@ -0,0 +1,15 @@
{
lib,
racket,
buildRacketPackage,
}: {
packages,
}: buildRacketPackage {
pname = "env";
version = "0";
unpackPhase = "touch nix-racket-env-only";
dependencies = packages;
tetheredInstallation = true;
}

7
racket-install-hook.nix Normal file
View File

@ -0,0 +1,7 @@
{
racket,
makeSetupHook,
}: makeSetupHook {
name = "racket-install-hook";
propagatedBuildInputs = [ racket ];
} ./racket-install-hook.sh

153
racket-install-hook.sh Normal file
View File

@ -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 - <<EOF
(require racket/function racket/hash racket/list racket/pretty racket/string racket/match)
(define out (getenv "out"))
(define pkgs (path-list-string->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