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/qx-package-schema.js"; schemaMainPathDefault = "dist/qx-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 = "@qx-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:" " \"qx-package@*\":" " dependencies:" ] ++ pkgs.lib.mapAttrsToList ( name: _: " \"@qx-package-schemas/${name}\": \"portal:./qx-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" qx-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="qx-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.__qx = ${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 = '' qx_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 "qx-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 qx-package-schemas ${schemaSupport.schemaLinkCommands} ''; buildPhase = (old.buildPhase or "") + ( if buildCommand != null then '' runHook preBuildQx ${buildCommand} runHook postBuildQx '' else "" ); postFixup = (old.postFixup or "") + runtimeWrap; postInstall = (old.postInstall or "") + '' pkg_root="$out/libexec/${old.name}" mkdir -p "$pkg_root/qx-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 = { "qx-prepare" = prepareShell; }; }; mkQxPackageFlake = { 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 "qx-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 qx-package-schemas ${schemaSupport.schemaLinkCommands} } base="$(find_schema_root || true)" if [ -n "$base" ]; then (cd "$base/src" && link_schemas && qx_package_schemas_update_yarnrc) if [ -d "$base/schema" ]; then ln -sfn "$base/schema" "$base/src/qx-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; }; }; } ); in { inherit mkQxPackageFlake mkTsPackageServer mkSchemaSupport; }