Files
quixos-nix-helpers/quixos-package-helpers.nix
T
2026-07-12 12:21:21 -07:00

680 lines
21 KiB
Nix

let
mkSchemaSupport =
{
pkgs,
inputs,
system,
self ? null,
flakeRoot,
schemaDir,
schemaPrefix ? "packageSchema_",
schemaBuildCommand ? "yarn build",
schemaTypecheckCommand ? "yarn typecheck",
nodejs ? pkgs.nodejs_24,
git ? pkgs.git,
}:
let
schemaPackageJson = builtins.fromJSON (builtins.readFile "${schemaDir}/package.json");
schemaMainPath = schemaPackageJson.main or "dist/quixos-package-schema.js";
schemaMainPathDefault = "dist/quixos-package-schema.js";
schemaPackageName = schemaPackageJson.name or "schema";
selfSchemaName =
if pkgs.lib.hasInfix "/" schemaPackageName
then pkgs.lib.last (pkgs.lib.splitString "/" schemaPackageName)
else schemaPackageName;
flakeText =
builtins.replaceStrings
[ "\n" "\r" "\t" ]
[ " " " " " " ]
(builtins.readFile "${flakeRoot}/flake.nix");
getInputUrl = inputName:
let
pattern1 = ".*" + inputName + "[[:space:]]*\\.url[[:space:]]*=[[:space:]]*\"([^\"]+)\".*";
pattern2 = ".*" + inputName + "[[:space:]]*=[[:space:]]*\\{[^}]*url[[:space:]]*=[[:space:]]*\"([^\"]+)\".*";
match1 = builtins.match pattern1 flakeText;
match2 = if match1 != null then match1 else builtins.match pattern2 flakeText;
in if match2 == null then null else builtins.elemAt match2 0;
schemaBase =
(pkgs.callPackage "${schemaDir}/yarn-project.nix" {
inherit nodejs git;
})
{
# avoid pulling .git, etc.
src = pkgs.lib.cleanSource schemaDir;
};
schema = schemaBase.overrideAttrs (old: {
buildPhase = (old.buildPhase or "") + ''
runHook preBuild
${schemaBuildCommand}
runHook postBuild
'';
});
schemaCheck = schema.overrideAttrs (old: {
doCheck = true;
checkPhase = (old.checkPhase or "") + ''
runHook preCheck
${schemaTypecheckCommand}
runHook postCheck
'';
});
schemaInputs =
pkgs.lib.filterAttrs (name: _: pkgs.lib.hasPrefix schemaPrefix name) inputs;
decodeSchemaInputName = encodedName:
builtins.replaceStrings [ "__" ] [ "." ] encodedName;
schemaPackagesFromInputs =
pkgs.lib.mapAttrs'
(name: flake:
let
schemaName =
decodeSchemaInputName (pkgs.lib.removePrefix schemaPrefix name);
in
{
name = schemaName;
value = flake.packages.${system}.schema;
})
schemaInputs;
schemaPackages = schemaPackagesFromInputs // { "${selfSchemaName}" = schema; };
schemaMetaByName =
pkgs.lib.mapAttrs'
(name: flake:
let
schemaName =
decodeSchemaInputName (pkgs.lib.removePrefix schemaPrefix name);
sourceInfo = if flake ? sourceInfo then flake.sourceInfo else { };
sourceRev = sourceInfo.rev or null;
inputUrl = getInputUrl name;
urlBase =
if inputUrl == null
then null
else builtins.head (pkgs.lib.splitString "?" inputUrl);
isDisallowed =
inputUrl == null
|| pkgs.lib.hasPrefix "path:" inputUrl
|| pkgs.lib.hasPrefix "file:" inputUrl
|| pkgs.lib.hasPrefix "git+file:" inputUrl;
flakeRef =
if isDisallowed || sourceRev == null || urlBase == null
then null
else urlBase + "?rev=" + sourceRev;
meta =
{
name = "@quixos-package-schemas/${schemaName}";
type = sourceInfo.type or null;
url = inputUrl;
rev = sourceRev;
flakeRef = flakeRef;
};
in
if flakeRef == null
then throw "Schema input ${schemaName} must be a git flake with url+rev"
else {
name = schemaName;
value = meta;
})
schemaInputs;
schemaMetaJsonByName =
pkgs.lib.mapAttrs (_: meta: builtins.toJSON meta) schemaMetaByName;
schemaExtensionsBlock = pkgs.lib.concatStringsSep "\n" (
[
"packageExtensions:"
" \"quixos-package@*\":"
" dependencies:"
]
++ pkgs.lib.mapAttrsToList (
name: _: " \"@quixos-package-schemas/${name}\": \"portal:./quixos-package-schemas/${name}\""
) schemaPackages
);
schemaExtensionsBlockEscaped = pkgs.lib.escapeShellArg schemaExtensionsBlock;
schemaLinkCommands = pkgs.lib.concatStringsSep "\n" (
pkgs.lib.mapAttrsToList (name: drv: ''
schema_pkg="$(ls -d ${drv}/libexec/* 2>/dev/null | head -n1 || true)"
if [ -n "$schema_pkg" ]; then
ln -sfn "$schema_pkg" quixos-package-schemas/${name}
fi
'') schemaPackages
);
schemaInstallCommands = pkgs.lib.concatStringsSep "\n" (
pkgs.lib.mapAttrsToList (name: drv:
let
metaJson =
if pkgs.lib.hasAttr name schemaMetaJsonByName
then schemaMetaJsonByName.${name}
else null;
in
''
schema_pkg="$(ls -d ${drv}/libexec/* | head -n1)"
dest="quixos-package-schemas/${name}"
rm -rf "$dest"
cp -R --no-preserve=mode,ownership "$schema_pkg" "$dest"
chmod -R u+w "$dest"
''
+ (
if metaJson != null
then ''
schema_main="${schemaMainPathDefault}"
if [ -f "$dest/$schema_main" ]; then
block_tmp="$(mktemp -p "$dest")"
tmp_out="$(mktemp -p "$dest")"
cat > "$block_tmp" <<'EOF'
packageSchema.__quixos = ${metaJson};
EOF
chmod u+w "$dest/$schema_main"
if grep -q '^export default ' "$dest/$schema_main"; then
awk -v blockFile="$block_tmp" '
BEGIN {
while ((getline line < blockFile) > 0) {
block = block line "\n"
}
}
!inserted && /^export default / { printf "%s", block; inserted=1 }
{ print }
' "$dest/$schema_main" > "$tmp_out"
cat "$tmp_out" > "$dest/$schema_main"
elif grep -q '^//# sourceMappingURL=' "$dest/$schema_main"; then
awk -v blockFile="$block_tmp" '
BEGIN {
while ((getline line < blockFile) > 0) {
block = block line "\n"
}
}
/^\/\/# sourceMappingURL=/ { printf "%s", block; print; next }
{ print }
' "$dest/$schema_main" > "$tmp_out"
cat "$tmp_out" > "$dest/$schema_main"
else
printf '\n' >> "$dest/$schema_main"
cat "$block_tmp" >> "$dest/$schema_main"
fi
rm -f "$block_tmp" "$tmp_out"
fi
''
else ""
)
) schemaPackages
);
updateYarnrcScript = ''
quixos_package_schemas_update_yarnrc() {
local yarnrc=".yarnrc.yml"
local tmp="$(mktemp)"
local block_tmp="$(mktemp)"
printf '%s\n' ${schemaExtensionsBlockEscaped} > "$block_tmp"
awk -v blockFile="$block_tmp" '
BEGIN {
while ((getline line < blockFile) > 0) {
block = block line "\n"
}
}
/^# BEGIN packageExtensions/ {print; printf "%s", block; skip=1; next}
/^# END packageExtensions/ {skip=0; print; next}
!skip {print}
' "$yarnrc" > "$tmp"
mv "$tmp" "$yarnrc"
rm -f "$block_tmp"
}
'';
in
{
inherit
schema
schemaCheck
schemaPackages
schemaLinkCommands
schemaInstallCommands
updateYarnrcScript
schemaExtensionsBlockEscaped
selfSchemaName;
};
mkTsPackageServer =
{
srcDir,
serverBin ? "server",
nodejs ? null,
git ? null,
templatePreparePackages ? null,
buildCommand ? null,
typecheckCommand ? "yarn typecheck",
serverOverrides ? (old: { }),
}:
{
pkgs,
schemaSupport,
extraBuildInputs ? [ ],
extraNativeBuildInputs ? [ ],
extraRuntimePackages ? [ ],
}:
let
nodejs' = if nodejs != null then nodejs else pkgs.nodejs_24;
git' = if git != null then git else pkgs.git;
templatePreparePackages' =
if templatePreparePackages != null
then templatePreparePackages
else [
pkgs.findutils
git'
pkgs.gnused
nodejs'
pkgs.yarn-berry_4
];
packageJson = builtins.fromJSON (builtins.readFile "${srcDir}/package.json");
packageName = packageJson.name or "quixos-package";
base =
(pkgs.callPackage "${srcDir}/yarn-project.nix" {
nodejs = nodejs';
git = git';
})
{
# avoid pulling .git, etc.
src = pkgs.lib.cleanSource srcDir;
};
runtimeBinPath = pkgs.lib.makeBinPath extraRuntimePackages;
runtimeLibPath = pkgs.lib.makeLibraryPath extraRuntimePackages;
runtimeWrap =
if extraRuntimePackages == [ ]
then ""
else ''
wrapProgram "$out/bin/${serverBin}" \
--prefix PATH : ${runtimeBinPath} \
--prefix LD_LIBRARY_PATH : ${runtimeLibPath}
'';
server = base.overrideAttrs (old:
let
extraAttrs = serverOverrides old;
in
{
buildInputs = (old.buildInputs or [ ]) ++ extraBuildInputs;
nativeBuildInputs = (old.nativeBuildInputs or [ ]) ++ [
pkgs.python3
pkgs.gnumake
pkgs.gcc
pkgs.pkg-config
pkgs.makeWrapper
] ++ extraNativeBuildInputs;
# node-gyp will look at these
PYTHON = "${pkgs.python3}/bin/python3";
npm_config_python = "${pkgs.python3}/bin/python3";
preConfigure = (old.preConfigure or "") + ''
mkdir -p quixos-package-schemas
${schemaSupport.schemaLinkCommands}
'';
buildPhase =
(old.buildPhase or "")
+ (
if buildCommand != null
then ''
runHook preBuildQuixos
${buildCommand}
runHook postBuildQuixos
''
else ""
);
postFixup = (old.postFixup or "") + runtimeWrap;
postInstall = (old.postInstall or "") + ''
pkg_root="$out/libexec/${old.name}"
mkdir -p "$pkg_root/quixos-package-schemas"
(cd "$pkg_root" && ${schemaSupport.schemaInstallCommands})
'';
} // extraAttrs
);
check = server.overrideAttrs (old: {
doCheck = true;
checkPhase = (old.checkPhase or "") + ''
runHook preCheck
${typecheckCommand}
runHook postCheck
'';
});
prepareShell = pkgs.mkShell {
packages = templatePreparePackages';
};
in
{
inherit server check packageName serverBin;
devShells = {
"quixos-prepare" = prepareShell;
};
};
mkQuixosPackageFlake =
{
self ? null,
inputs,
nixpkgs,
flake-utils,
flakeRoot,
schemaDir,
schemaPrefix ? "packageSchema_",
serverBuilder,
packageName ? null,
extraBuildInputs ? (pkgs: [ ]),
extraNativeBuildInputs ? (pkgs: [ ]),
extraRuntimePackages ? (pkgs: [ ]),
extraDevShellPackages ? (pkgs: [ ]),
enableYarnrcHook ? true,
extraDevShellHook ? "",
appProgram ? null,
}:
flake-utils.lib.eachDefaultSystem (
system:
let
pkgs = import nixpkgs { inherit system; };
schemaSupport = mkSchemaSupport {
inherit pkgs inputs system schemaDir schemaPrefix self flakeRoot;
};
normalizeList = value:
if builtins.isFunction value
then value pkgs
else value;
serverResult = serverBuilder {
inherit pkgs schemaSupport;
extraBuildInputs = normalizeList extraBuildInputs;
extraNativeBuildInputs = normalizeList extraNativeBuildInputs;
extraRuntimePackages = normalizeList extraRuntimePackages;
};
combinedName = pkgs.lib.strings.sanitizeDerivationName (
if packageName != null
then packageName
else if serverResult ? packageName
then serverResult.packageName
else "quixos-package"
);
packageServer = serverResult.server;
packageServerCheck = if serverResult ? check then serverResult.check else null;
appProgramFinal =
if appProgram != null
then appProgram
else if serverResult ? appProgram
then serverResult.appProgram
else "${packageServer}/bin/${serverResult.serverBin or "server"}";
devShellHookBase =
if enableYarnrcHook
then ''
find_schema_root() {
dir="$PWD"
while [ "$dir" != "/" ]; do
if [ -f "$dir/src/.yarnrc.yml" ] && [ -d "$dir/schema" ]; then
printf "%s\n" "$dir"
return 0
fi
dir="$(dirname "$dir")"
done
return 1
}
${schemaSupport.updateYarnrcScript}
link_schemas() {
mkdir -p quixos-package-schemas
${schemaSupport.schemaLinkCommands}
}
base="$(find_schema_root || true)"
if [ -n "$base" ]; then
(cd "$base/src" && link_schemas && quixos_package_schemas_update_yarnrc)
if [ -d "$base/schema" ]; then
ln -sfn "$base/schema" "$base/src/quixos-package-schemas/${schemaSupport.selfSchemaName}"
fi
fi
''
else "";
devShellHook = devShellHookBase + extraDevShellHook;
devShellPackages = normalizeList extraDevShellPackages;
checks =
{ schema = schemaSupport.schemaCheck; }
// (if packageServerCheck != null then { default = packageServerCheck; } else { });
extraDevShells = if serverResult ? devShells then serverResult.devShells else { };
in
{
packages.default = pkgs.symlinkJoin {
name = combinedName;
paths = [ packageServer schemaSupport.schema ];
};
packages.server = packageServer;
packages.schema = schemaSupport.schema;
apps.default = {
type = "app";
program = appProgramFinal;
meta.description = "Quixos package server";
};
checks = checks;
devShells = extraDevShells // {
default = pkgs.mkShell {
inputsFrom = [ packageServer ] ++ builtins.attrValues schemaSupport.schemaPackages;
packages = devShellPackages;
shellHook = devShellHook;
};
};
}
);
mkCaminoSourcePackage =
{
pkgs,
src,
name ? "quixos-package-source",
}:
pkgs.stdenvNoCC.mkDerivation {
inherit name;
src = pkgs.lib.cleanSource src;
dontConfigure = true;
dontBuild = true;
installPhase = ''
runHook preInstall
cp -R . "$out"
runHook postInstall
'';
};
mkCaminoTsYarnNixifyFlake =
{
inputs,
nixpkgs,
flake-utils,
packageRoot,
packageName ? null,
sourceName ? "quixos-package-source",
promptName ? null,
nodejsAttr ? "nodejs_24",
buildCommand ? "yarn build",
sourcePortals ? { },
buildEnv ? { },
nativeBuildInputs ? [ ],
devShellPackages ? [ ],
devShellHook ? "",
bundle ? null,
installServer ? null,
}:
flake-utils.lib.eachDefaultSystem (
system:
let
pkgs = import nixpkgs { inherit system; };
lib = pkgs.lib;
nodejs = pkgs.${nodejsAttr};
callOption = value:
if builtins.isFunction value
then value { inherit inputs pkgs system; }
else value;
packageJson = builtins.fromJSON (builtins.readFile "${packageRoot}/package.json");
packageNameFinal = if packageName != null then packageName else packageJson.name or "quixos-package";
promptNameFinal =
if promptName != null
then promptName
else lib.strings.sanitizeDerivationName packageNameFinal;
sourcePackage = mkCaminoSourcePackage {
inherit pkgs;
src = packageRoot;
name = sourceName;
};
exportsFor = attrs:
lib.concatStringsSep "\n" (
lib.mapAttrsToList
(name: value: "export ${name}=${lib.escapeShellArg (toString value)}")
attrs
);
portalLinksFor = attrs:
lib.concatStringsSep "\n" (
lib.mapAttrsToList
(target: source: "ln -sfn ${source} ${lib.escapeShellArg target}")
attrs
);
bundleConfig = if bundle == null then { } else bundle;
bundleOutfile = bundleConfig.outfile or "server.mjs";
bundlePlatform = bundleConfig.platform or "node";
bundleTarget = bundleConfig.target or "node24";
bundleFormat = bundleConfig.format or "esm";
bundleBanner = bundleConfig.banner or nodeRequireBanner;
nodeRequireBanner = "import { createRequire } from 'module';const require = createRequire(import.meta.url);";
bundleCommand =
if bundle == null
then ""
else ''
esbuild ${lib.escapeShellArg bundleConfig.entry} \
--bundle \
--platform=${bundlePlatform} \
--target=${bundleTarget} \
--format=${bundleFormat} \
${lib.optionalString (bundleConfig.preserveSymlinks or true) "--preserve-symlinks \\"}
--banner:js=${lib.escapeShellArg bundleBanner} \
--outfile=${lib.escapeShellArg bundleOutfile}
'';
installConfig = if installServer == null then { } else installServer;
serverBin = installConfig.binName or "server";
serverLibexecName =
installConfig.libexecName or (lib.strings.sanitizeDerivationName packageNameFinal);
serverFile = installConfig.serverFile or bundleOutfile;
descriptorPath = installConfig.descriptorPath or "descriptor.quixos-package.txtpb";
installServerPhase =
if installServer == null
then null
else ''
runHook preInstall
install -Dm755 ${lib.escapeShellArg serverFile} "$out/libexec/${serverLibexecName}/${serverFile}"
mkdir -p "$out/bin"
cat > "$out/bin/${serverBin}" <<EOF
#!${pkgs.runtimeShell}
exec ${nodejs}/bin/node "$out/libexec/${serverLibexecName}/${serverFile}" "\$@"
EOF
chmod +x "$out/bin/${serverBin}"
${lib.optionalString (installConfig ? descriptorPath && descriptorPath != null) ''
cp ${lib.escapeShellArg descriptorPath} "$out/${descriptorPath}"
''}
runHook postInstall
'';
project = (pkgs.callPackage "${packageRoot}/yarn-project.nix" { inherit nodejs; }) {
src = packageRoot;
overrideAttrs = old: {
nativeBuildInputs =
(old.nativeBuildInputs or [ ])
++ lib.optional (bundle != null) pkgs.esbuild
++ callOption nativeBuildInputs;
preConfigure = (old.preConfigure or "") + ''
${portalLinksFor (callOption sourcePortals)}
'';
buildPhase = ''
runHook preBuild
${exportsFor (callOption buildEnv)}
${buildCommand}
${bundleCommand}
runHook postBuild
'';
} // lib.optionalAttrs (installServerPhase != null) {
installPhase = installServerPhase;
};
};
devShellHookBase = ''
if [ -n "''${PS1-}" ]; then
if [ -n "''${QX_DEV_SHELL_PROMPT-}" ]; then
PS1="''${PS1#\[qx:''${QX_DEV_SHELL_PROMPT}\] }"
fi
export QX_DEV_SHELL_PROMPT=${lib.escapeShellArg promptNameFinal}
PS1="[qx:${promptNameFinal}] $PS1"
fi
'';
maybeServerOutputs =
if installServer == null
then { }
else {
packages.server = project;
apps.default = {
type = "app";
program = "${project}/bin/${serverBin}";
};
};
in
{
packages.default = project;
packages.source = sourcePackage;
devShells.default = pkgs.mkShell {
packages = [
nodejs
pkgs.yarn-berry_4
] ++ callOption devShellPackages;
shellHook = devShellHookBase + callOption devShellHook;
};
} // maybeServerOutputs
);
in
{
inherit
mkQuixosPackageFlake
mkTsPackageServer
mkSchemaSupport
mkCaminoSourcePackage
mkCaminoTsYarnNixifyFlake;
}