diff --git a/src/subsystems/php/builders/simple/default.nix b/src/subsystems/php/builders/simple/default.nix index bdd7a92411..01e133fcce 100644 --- a/src/subsystems/php/builders/simple/default.nix +++ b/src/subsystems/php/builders/simple/default.nix @@ -7,6 +7,7 @@ stdenv, # dream2nix inputs externals, + callPackageDream, ... }: { ### FUNCTIONS @@ -36,6 +37,29 @@ } @ args: let l = lib // builtins; + inherit (callPackageDream ../../semver.nix {}) satisfies; + + # php with required extensions + php = + if satisfies pkgs.php81.version subsystemAttrs.phpSemver + then + pkgs.php81.withExtensions ( + { + all, + enabled, + }: + l.unique (enabled + ++ (l.attrValues (l.filterAttrs (e: _: l.elem e subsystemAttrs.phpExtensions) all))) + ) + else + l.abort '' + Error: incompatible php versions. + Package "${defaultPackageName}" defines required php version: + "php": "${subsystemAttrs.phpSemver}" + Using php version "${pkgs.php81.version}" from attribute "pkgs.php81". + ''; + composer = php.packages.composer; + # packages to export packages = {default = packages.${defaultPackageName};} @@ -91,11 +115,11 @@ nativeBuildInputs = with pkgs; [ jq - php81Packages.composer + composer ]; buildInputs = with pkgs; [ - php81 - php81Packages.composer + php + composer ]; dontConfigure = true; @@ -130,23 +154,27 @@ popd ''; installPhase = '' - if [ -d $PKG_OUT/bin ] - then + pushd $PKG_OUT + + BINS=$(jq -rcM "(.bin // [])[]" composer.json) + for bin in $BINS + do mkdir -p $out/bin - for bin in $(ls $PKG_OUT/bin) - do - ln -s $PKG_OUT/bin/$bin $out/bin/$bin - done - fi + pushd $out/bin + ln -s $PKG_OUT/$bin + popd + done + + popd ''; passthru.devShell = import ./devShell.nix { inherit name pkg + php ; inherit (pkgs) mkShell; - php = pkgs.php81; }; }; in diff --git a/src/subsystems/php/builders/simple/devShell.nix b/src/subsystems/php/builders/simple/devShell.nix index 604caf2b82..1d4224079e 100644 --- a/src/subsystems/php/builders/simple/devShell.nix +++ b/src/subsystems/php/builders/simple/devShell.nix @@ -10,7 +10,7 @@ mkShell { ]; shellHook = let vendorDir = - pkg.overrideAttrs (old: { + pkg.overrideAttrs (_: { dontInstall = true; }) + "/lib/vendor/${name}/vendor"; diff --git a/src/subsystems/php/semver.nix b/src/subsystems/php/semver.nix index 7689812e52..8f17f0440a 100644 --- a/src/subsystems/php/semver.nix +++ b/src/subsystems/php/semver.nix @@ -1,4 +1,4 @@ -{lib}: let +{lib, ...}: let l = lib // builtins; # Replace a list entry at defined index with set value @@ -20,8 +20,7 @@ mkCaretComparison = version: v: let ver = builtins.splitVersion v; major = l.toInt (l.head ver); - minor = builtins.toString (l.toInt (l.head ver) + 1); - upper = builtins.concatStringsSep "." (ireplace 0 minor ver); + upper = builtins.toString (l.toInt (l.head ver) + 1); in if major == 0 then mkTildeComparison version v @@ -111,12 +110,80 @@ else throw ''Constraint "${constraintStr}" could not be parsed'' ); - satisfies = version: constraint: let + satisfiesSingleInternal = version: constraint: let inherit (parseConstraint constraint) ops v; in if ops.t == "-" then (operators."${ops.l}" version v.vl && operators."${ops.u}" version v.vu) else operators."${ops.t}" version v; -in { - inherit satisfies; + + # remove v from version strings: ^v1.2.3 -> ^1.2.3 + # remove branch suffix: ^1.2.x-dev -> ^1.2 + satisfiesSingle = version: constraint: let + removeSuffix = c: let + m = l.match "^(.*)[-][[:alpha:]]+$" c; + in + if m != null && l.length m >= 0 + then l.head m + else c; + wildcard = c: let + m = l.match "^([[:d:]]+.*)[.][*x]$" c; + in + if m != null && l.length m >= 0 + then "^${l.head m}" + else c; + removeV = c: let + m = l.match "^(.)*v([[:d:]]+[.].*)$" c; + in + if m != null && l.length m > 0 + then l.concatStrings m + else c; + isVersionLike = c: let + m = l.match "^([0-9><=!-^~*]*)$" c; + in + m != null && l.length m > 0; + cleanConstraint = removeV (wildcard (removeSuffix (l.removePrefix "dev-" constraint))); + cleanVersion = l.removePrefix "v" (wildcard (removeSuffix version)); + in + (l.elem constraint ["" "*" "@dev" "@master" "@dev-master"]) + || (version == constraint) + || ((isVersionLike cleanConstraint) && (satisfiesSingleInternal cleanVersion cleanConstraint)); + + trim = s: l.head (l.match "^[[:space:]]*(.*[^[:space:]])[[:space:]]*$" s); + splitAlternatives = v: let + # handle version alternatives: ^1.2 || ^2.0 + clean = l.replaceStrings ["||"] ["|"] v; + in + map trim (l.splitString "|" clean); + splitConjunctives = v: let + clean = + l.replaceStrings + ["," " - " " -" "- " " as "] + [" " "-" "-" "-" "##"] + v; + cleanInlineAlias = v: let + m = l.match "^(.*)[#][#](.*)$" v; + in + if m != null && l.length m > 0 + then l.head m + else v; + in + map (x: trim (cleanInlineAlias x)) (l.splitString " " clean); +in rec { + # matching a version with semver + # 1.0.2 (~1.0.1 || >=2.1 <2.4) + satisfies = version: constraint: + l.any + (c: + l.all + (satisfiesSingle version) + (splitConjunctives c)) + (splitAlternatives constraint); + + # matching multiversion like the one in `provide` with semver + # (1.0|2.0) (^2.0 || 3.2 - 3.6) + multiSatisfies = multiversion: constraint: + l.any + (version: satisfies version constraint) + (splitAlternatives multiversion); } diff --git a/src/subsystems/php/translators/composer-json/default.nix b/src/subsystems/php/translators/composer-json/default.nix index 27c74dcadc..226f4a5283 100644 --- a/src/subsystems/php/translators/composer-json/default.nix +++ b/src/subsystems/php/translators/composer-json/default.nix @@ -2,9 +2,7 @@ dlib, lib, ... -}: let - l = lib // builtins; -in { +}: { type = "impure"; # A derivation which outputs a single executable at `$out`. @@ -27,7 +25,6 @@ in { coreutils, jq, phpPackages, - writeScriptBin, ... }: utils.writePureShellScript diff --git a/src/subsystems/php/translators/composer-lock/default.nix b/src/subsystems/php/translators/composer-lock/default.nix index 077f64dbc2..1b48d16ce1 100644 --- a/src/subsystems/php/translators/composer-lock/default.nix +++ b/src/subsystems/php/translators/composer-lock/default.nix @@ -94,6 +94,12 @@ in { noDev, ... } @ args: let + inherit + (callPackageDream ../../semver.nix {}) + satisfies + multiSatisfies + ; + # get the root source and project source rootSource = tree.fullPath; projectSource = "${tree.fullPath}/${project.relPath}"; @@ -102,120 +108,108 @@ in { composerJson = (projectTree.getNodeFromPath "composer.json").jsonContent; composerLock = (projectTree.getNodeFromPath "composer.lock").jsonContent; - inherit - (callPackageDream ../../utils.nix {}) - satisfiesSemver - multiSatisfiesSemver - ; + # toplevel php semver + phpSemver = composerJson.require."php" or "*"; + # all the php extensions + phpExtensions = let + allDepNames = l.flatten (map (x: l.attrNames (getRequire x)) packages); + extensions = l.unique (l.filter (l.hasPrefix "ext-") allDepNames); + in + map (l.removePrefix "ext-") extensions; + + # get cleaned pkg attributes + getRequire = pkg: + l.mapAttrs + (_: version: resolvePkgVersion pkg version) + (pkg.require or {}); + getProvide = pkg: + l.mapAttrs + (_: version: resolvePkgVersion pkg version) + (pkg.provide or {}); + getReplace = pkg: + l.mapAttrs + (_: version: resolvePkgVersion pkg version) + (pkg.replace or {}); + + resolvePkgVersion = pkg: version: + if version == "self.version" + then pkg.version + else version; + # project package + toplevelPackage = { + name = project.name; + version = composerJson.version or "unknown"; + source = { + type = "path"; + path = projectSource; + }; + require = + (l.optionalAttrs (!noDev) (composerJson.require-dev or {})) + // composerJson.require; + }; # all the packages packages = - composerLock.packages - ++ ( - if noDev - then [] - else composerLock.packages-dev - ); - - # packages with replacements applied + # Add the top-level package, this is not written in composer.lock + [toplevelPackage] + ++ composerLock.packages + ++ (l.optionals (!noDev) (composerLock.packages-dev or [])); + # packages with replace/provide applied resolvedPackages = let - getProvide = pkg: (pkg.provide or {}); - getReplace = pkg: let - resolveVersion = _: version: - if version == "self.version" - then pkg.version - else version; - in - l.mapAttrs resolveVersion (pkg.replace or {}); - provide = pkg: dep: let - requirements = getDependencies pkg; - providements = getProvide dep; - cleanRequirements = - l.filterAttrs ( - name: semver: - !((providements ? "${name}") - && (multiSatisfiesSemver providements."${name}" semver)) - ) - requirements; - in - pkg - // { - require = - cleanRequirements - // ( - if requirements != cleanRequirements - then {"${dep.name}" = "${dep.version}";} - else {} - ); - }; - replace = pkg: dep: let - requirements = getDependencies pkg; - replacements = getReplace dep; - cleanRequirements = + apply = pkg: dep: candidates: let + original = getRequire pkg; + applied = l.filterAttrs ( name: semver: - !((replacements ? "${name}") - && (satisfiesSemver replacements."${name}" semver)) + !((candidates ? "${name}") && (multiSatisfies candidates."${name}" semver)) ) - requirements; + original; in pkg // { require = - cleanRequirements + applied // ( - if requirements != cleanRequirements - then {"${dep.name}" = "${dep.version}";} - else {} + l.optionalAttrs + (applied != original) + {"${dep.name}" = "${dep.version}";} ); }; - doReplace = pkg: l.foldl replace pkg packages; - doProvide = pkg: l.foldl provide pkg packages; dropMissing = pkgs: let doDropMissing = pkg: pkg // { require = l.filterAttrs - (name: semver: l.any (pkg: (pkg.name == name) && (satisfiesSemver pkg.version semver)) pkgs) - (getDependencies pkg); + (name: semver: l.any (pkg: (pkg.name == name) && (satisfies pkg.version semver)) pkgs) + (getRequire pkg); }; in map doDropMissing pkgs; - resolve = pkg: (doProvide (doReplace pkg)); - in - dropMissing (map resolve packages); - - # toplevel php semver - phpSemver = composerJson.require."php" or "*"; - # all the php extensions - phpExtensions = let - all = map (pkg: l.attrsets.attrNames (getDependencies pkg)) resolvedPackages; - flat = l.lists.flatten all; - extensions = l.filter (l.strings.hasPrefix "ext-") flat; + doReplace = pkg: + l.foldl + (pkg: dep: apply pkg dep (getProvide dep)) + pkg + packages; + doProvide = pkg: + l.foldl + (pkg: dep: apply pkg dep (getReplace dep)) + pkg + packages; in - map (l.strings.removePrefix "ext-") (l.lists.unique extensions); - - # get dependencies - getDependencies = pkg: - l.mapAttrs - (name: version: - if version == "self.version" - then pkg.version - else version) - (pkg.require or {}); + dropMissing (map (pkg: (doProvide (doReplace pkg))) packages); # resolve semvers into exact versions pinPackages = pkgs: let clean = requires: l.filterAttrs (name: _: - (l.all (x: name != x) ["php" "composer/composer" "composer-runtime-api"]) + !(l.elem name ["php" "composer/composer" "composer-runtime-api"]) && !(l.strings.hasPrefix "ext-" name)) requires; doPin = name: semver: (l.head - (l.filter (dep: satisfiesSemver dep.version semver) + (l.filter (dep: satisfies dep.version semver) (l.filter (dep: dep.name == name) resolvedPackages))) .version; @@ -245,7 +239,7 @@ in { }; # name of the default package - defaultPackage = project.name; + defaultPackage = toplevelPackage.name; /* List the package candidates which should be exposed to the user. @@ -253,7 +247,7 @@ in { Users will not be interested in all individual dependencies. */ exportedPackages = { - "${defaultPackage}" = composerJson.version or "unknown"; + "${defaultPackage}" = toplevelPackage.version; }; /* @@ -261,27 +255,7 @@ in { If the upstream format is a deep attrset, this list should contain a flattened representation of all entries. */ - serializedRawObjects = pinPackages ( - [ - # Add the top-level package, this is not written in composer.lock - { - name = defaultPackage; - version = exportedPackages."${defaultPackage}"; - source = { - type = "path"; - path = projectSource; - }; - require = - ( - if noDev - then {} - else composerJson.require-dev or {} - ) - // composerJson.require; - } - ] - ++ resolvedPackages - ); + serializedRawObjects = pinPackages resolvedPackages; /* Define extractor functions which each extract one property from @@ -301,7 +275,7 @@ in { dependencies = rawObj: finalObj: l.attrsets.mapAttrsToList (name: version: {inherit name version;}) - (getDependencies rawObj); + (getRequire rawObj); sourceSpec = rawObj: finalObj: if rawObj.source.type == "path" diff --git a/src/subsystems/php/utils.nix b/src/subsystems/php/utils.nix deleted file mode 100644 index 4634884dda..0000000000 --- a/src/subsystems/php/utils.nix +++ /dev/null @@ -1,77 +0,0 @@ -{lib, ...}: let - l = lib // builtins; - - inherit (import ./semver.nix {inherit lib;}) satisfies; - - # composer.lock uses a less strict semver interpretation - # ~1.2 -> >=1.2 <2.0.0 (instead of >=1.2.0 <1.3.0) - # ~1 -> >=1.0 <2.0.0 - # this is identical with ^1.2 in the semver standard - # - # remove v from version strings: ^v1.2.3 -> ^1.2.3 - # - # remove branch suffix: ^1.2.x-dev -> ^1.2 - # - satisfiesSemverSingle = version: constraint: let - removeSuffix = c: let - m = l.match "^(.*)[-][[:alpha:]]+$" c; - in - if m != null && l.length m >= 0 - then l.head m - else c; - removeX = l.strings.removeSuffix ".x"; - tilde = c: let - m = l.match "^[~]([[:d:]]+.*)$" c; - in - if m != null && l.length m >= 0 - then "^${l.head m}" - else c; - wildcard = c: let - m = l.match "^([[:d:]]+.*)[.][*x]$" c; - in - if m != null && l.length m >= 0 - then "^${l.head m}" - else c; - removeV = c: let - m = l.match "^(.)*v([[:d:]]+[.].*)$" c; - in - if m != null && l.length m > 0 - then l.concatStrings m - else c; - cleanConstraint = removeV (wildcard (tilde (removeSuffix constraint))); - cleanVersion = removeX (l.removePrefix "v" (removeSuffix version)); - in - (l.any (x: constraint == x) ["*" "@dev" "@master" "@dev-master"]) - || (version == constraint) - || (satisfies cleanVersion cleanConstraint); - - trim = s: l.head (l.match "^[[:space:]]*(.*[^[:space:]])[[:space:]]*$" s); - splitAlternatives = v: let - # handle version alternatives: ^1.2 || ^2.0 - clean = l.replaceStrings ["||"] ["|"] v; - in - map trim (l.splitString "|" clean); - splitConjunctives = v: let - clean = l.replaceStrings ["," " - " " -" "- "] [" " "-" "-" "-"] v; - in - map trim (l.splitString " " clean); -in { - # 1.0.2 ~1.0.1 - # matching a version with semver - satisfiesSemver = version: constraint: - l.any - (c: - l.all - (satisfiesSemverSingle version) - (splitConjunctives c)) - (splitAlternatives constraint); - - # 1.0|2.0 ^2.0 - # matching multiversion like the one in `provide` with semver - multiSatisfiesSemver = multiversion: constraint: let - satisfies = v: c: (v == "") || (v == "*") || (satisfiesSemverSingle v c); - in - l.any - (c: l.any (v: l.all (satisfies v) (splitConjunctives c)) (splitAlternatives multiversion)) - (splitAlternatives constraint); -}