diff --git a/docs/src/subsystems/php.md b/docs/src/subsystems/php.md index b4b4f8b209..418b1e8785 100644 --- a/docs/src/subsystems/php.md +++ b/docs/src/subsystems/php.md @@ -1,11 +1,19 @@ # PHP subsystem -> !!! PHP support is a work in progress, and it is not yet usable (a -> builder is missing). You can track the progress in +> !!! PHP support is experimental. \ +> !!! You can track the progress in > [nix-community/dream2nix#240](https://github.com/nix-community/dream2nix/issues/240). This section documents the PHP subsystem. +## Example + +An example of building [composer](https://github.com/composer/composer) using dream2nix. + +```nix +{{#include ../../../examples/php_composer/flake.nix}} +``` + ## Translators ### composer-lock (pure) @@ -20,4 +28,6 @@ generate a dream2nix lockfile. ## Builders -None so far. +### simple (pure) (default) + +Builds the package including all its dependencies in a single derivation. diff --git a/examples/php_composer/flake.nix b/examples/php_composer/flake.nix new file mode 100644 index 0000000000..d39a0c494a --- /dev/null +++ b/examples/php_composer/flake.nix @@ -0,0 +1,22 @@ +{ + inputs = { + dream2nix.url = "github:nix-community/dream2nix"; + src.url = "github:composer/composer"; + src.flake = false; + }; + + outputs = { + self, + dream2nix, + src, + } @ inp: + (dream2nix.lib.makeFlakeOutputs { + systems = ["x86_64-linux"]; + config.projectRoot = ./.; + source = src; + settings = []; + }) + // { + # checks = self.packages; + }; +} diff --git a/src/lib/builders.nix b/src/lib/builders.nix index 22047a0fe6..5596bcf387 100644 --- a/src/lib/builders.nix +++ b/src/lib/builders.nix @@ -9,6 +9,7 @@ rust = "build-rust-package"; nodejs = "granular"; python = "simple-builder"; + php = "simple"; }; # TODO diff --git a/src/subsystems/php/builders/simple/default.nix b/src/subsystems/php/builders/simple/default.nix new file mode 100644 index 0000000000..bdd7a92411 --- /dev/null +++ b/src/subsystems/php/builders/simple/default.nix @@ -0,0 +1,158 @@ +{...}: { + type = "pure"; + + build = { + lib, + pkgs, + stdenv, + # dream2nix inputs + externals, + ... + }: { + ### FUNCTIONS + # AttrSet -> Bool) -> AttrSet -> [x] + getCyclicDependencies, # name: version: -> [ {name=; version=; } ] + getDependencies, # name: version: -> [ {name=; version=; } ] + getSource, # name: version: -> store-path + # to get information about the original source spec + getSourceSpec, # name: version: -> {type="git"; url=""; hash="";} + ### ATTRIBUTES + subsystemAttrs, # attrset + defaultPackageName, # string + defaultPackageVersion, # string + # all exported (top-level) package names and versions + # attrset of pname -> version, + packages, + # all existing package names and versions + # attrset of pname -> versions, + # where versions is a list of version strings + packageVersions, + # function which applies overrides to a package + # It must be applied by the builder to each individual derivation + # Example: + # produceDerivation name (mkDerivation {...}) + produceDerivation, + ... + } @ args: let + l = lib // builtins; + + # packages to export + packages = + {default = packages.${defaultPackageName};} + // ( + l.mapAttrs + (name: version: {"${version}" = makePackage name version;}) + args.packages + ); + devShells = + {default = devShells.${defaultPackageName};} + // ( + l.mapAttrs + (name: version: packages.${name}.${version}.devShell) + args.packages + ); + + # Generates a derivation for a specific package name + version + makePackage = name: version: let + dependencies = getDependencies name version; + allDependencies = let + withKey = x: x // {key = "${x.name} ${x.version}";}; + in + l.genericClosure { + startSet = map withKey dependencies; + operator = dep: map withKey (getDependencies dep.name dep.version); + }; + + intoRepository = dep: { + type = "path"; + url = "${getSource dep.name dep.version}"; + options = { + versions = { + "${dep.name}" = "${dep.version}"; + }; + symlink = false; + }; + }; + repositories = l.flatten (map intoRepository allDependencies); + repositoriesString = + l.toJSON + (repositories ++ [{packagist = false;}]); + + versionString = + if version == "unknown" + then "0.0.0" + else version; + + pkg = stdenv.mkDerivation rec { + pname = l.strings.sanitizeDerivationName name; + inherit version; + + src = getSource name version; + + nativeBuildInputs = with pkgs; [ + jq + php81Packages.composer + ]; + buildInputs = with pkgs; [ + php81 + php81Packages.composer + ]; + + dontConfigure = true; + buildPhase = '' + # copy source + PKG_OUT=$out/lib/vendor/${name} + mkdir -p $PKG_OUT + pushd $PKG_OUT + cp -r ${src}/* . + + # remove composer.lock if exists + rm -f composer.lock + + # disable packagist, set path repositories + mv composer.json composer.json.orig + + cat <> $out/repositories.json + ${repositoriesString} + EOF + + jq \ + --slurpfile repositories $out/repositories.json \ + "(.repositories = \$repositories[0]) | \ + (.version = \"${versionString}\")" \ + composer.json.orig > composer.json + + # build + composer install --no-scripts + + # cleanup + rm $out/repositories.json + popd + ''; + installPhase = '' + if [ -d $PKG_OUT/bin ] + then + mkdir -p $out/bin + for bin in $(ls $PKG_OUT/bin) + do + ln -s $PKG_OUT/bin/$bin $out/bin/$bin + done + fi + ''; + + passthru.devShell = import ./devShell.nix { + inherit + name + pkg + ; + inherit (pkgs) mkShell; + php = pkgs.php81; + }; + }; + in + # apply packageOverrides to current derivation + produceDerivation name pkg; + in { + inherit packages devShells; + }; +} diff --git a/src/subsystems/php/builders/simple/devShell.nix b/src/subsystems/php/builders/simple/devShell.nix new file mode 100644 index 0000000000..604caf2b82 --- /dev/null +++ b/src/subsystems/php/builders/simple/devShell.nix @@ -0,0 +1,24 @@ +{ + name, + pkg, + mkShell, + php, +}: +mkShell { + buildInputs = [ + php + ]; + shellHook = let + vendorDir = + pkg.overrideAttrs (old: { + dontInstall = true; + }) + + "/lib/vendor/${name}/vendor"; + in '' + rm -rf ./vendor + mkdir vendor + cp -r ${vendorDir}/* vendor/ + chmod -R +w ./vendor + export PATH="$PATH:$(realpath ./vendor)/bin" + ''; +} diff --git a/src/subsystems/php/discoverers/default/default.nix b/src/subsystems/php/discoverers/default/default.nix new file mode 100644 index 0000000000..764ddf0769 --- /dev/null +++ b/src/subsystems/php/discoverers/default/default.nix @@ -0,0 +1,35 @@ +{ + dlib, + lib, + subsystem, + ... +}: let + l = lib // builtins; + + # get translators for the project + getTranslators = tree: + l.optional (tree.files ? "composer.lock") "composer-lock" + ++ ["composer-json"]; + + # discover php projects + discover = {tree}: let + currentProjectInfo = dlib.construct.discoveredProject { + inherit subsystem; + inherit (tree) relPath; + name = + tree.files."composer.json".jsonContent.name + or ( + if tree.relPath != "" + then tree.relPath + else "unknown" + ); + translators = getTranslators tree; + subsystemInfo = {}; + }; + in + if l.pathExists "${tree.fullPath}/composer.json" + then [currentProjectInfo] + else []; +in { + inherit discover; +} diff --git a/src/subsystems/php/semver.nix b/src/subsystems/php/semver.nix new file mode 100644 index 0000000000..7689812e52 --- /dev/null +++ b/src/subsystems/php/semver.nix @@ -0,0 +1,122 @@ +{lib}: let + l = lib // builtins; + + # Replace a list entry at defined index with set value + ireplace = idx: value: list: + l.genList (i: + if i == idx + then value + else (l.elemAt list i)) (l.length list); + + orBlank = x: + if x != null + then x + else ""; + + operators = let + mkComparison = ret: version: v: + builtins.compareVersions version v == ret; + + 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); + in + if major == 0 + then mkTildeComparison version v + else operators.">=" version v && operators."<" version upper; + + mkTildeComparison = version: v: let + ver = builtins.splitVersion v; + len = l.length ver; + truncated = + if len > 1 + then l.init ver + else ver; + idx = (l.length truncated) - 1; + minor = l.toString (l.toInt (l.elemAt truncated idx) + 1); + upper = l.concatStringsSep "." (ireplace idx minor truncated); + in + operators.">=" version v && operators."<" version upper; + in { + # Prefix operators + "==" = mkComparison 0; + ">" = mkComparison 1; + "<" = mkComparison (-1); + "!=" = v: c: !operators."==" v c; + ">=" = v: c: operators."==" v c || operators.">" v c; + "<=" = v: c: operators."==" v c || operators."<" v c; + # Semver specific operators + "~" = mkTildeComparison; + "^" = mkCaretComparison; + }; + + re = { + operators = "([=>="; + u = "<="; + }; + v = { + vl = orBlank (l.elemAt mIn 0); + vu = orBlank (l.elemAt mIn reLengths.version); + }; + } + else if mUpperBound != null + then { + ops = { + t = "-"; + l = l.elemAt mUpperBound 0; + u = "<"; + }; + v = { + vl = orBlank (l.elemAt mUpperBound reLengths.operators); + vu = orBlank (l.elemAt mUpperBound (reLengths.operators + reLengths.version)); + }; + } + else if mNone != null + then { + ops.t = "=="; + v = orBlank (l.elemAt mNone 0); + } + else throw ''Constraint "${constraintStr}" could not be parsed'' + ); + + satisfies = 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; +} diff --git a/src/subsystems/php/translators/composer-json/default.nix b/src/subsystems/php/translators/composer-json/default.nix index fa59bc1ddf..27c74dcadc 100644 --- a/src/subsystems/php/translators/composer-json/default.nix +++ b/src/subsystems/php/translators/composer-json/default.nix @@ -7,22 +7,6 @@ in { type = "impure"; - /* - Allow dream2nix to detect if a given directory contains a project - which can be translated with this translator. - Usually this can be done by checking for the existence of specific - file names or file endings. - - Alternatively a fully featured discoverer can be implemented under - `src/subsystems/{subsystem}/discoverers`. - This is recommended if more complex project structures need to be - discovered like, for example, workspace projects spanning over multiple - sub-directories - - If a fully featured discoverer exists, do not define `discoverProject`. - */ - discoverProject = tree: (l.pathExists "${tree.fullPath}/composer.json"); - # A derivation which outputs a single executable at `$out`. # The executable will be called by dream2nix for translation # The input format is specified in /specifications/translator-call-example.json. diff --git a/src/subsystems/php/translators/composer-lock/default.nix b/src/subsystems/php/translators/composer-lock/default.nix index 29576e100d..077f64dbc2 100644 --- a/src/subsystems/php/translators/composer-lock/default.nix +++ b/src/subsystems/php/translators/composer-lock/default.nix @@ -22,24 +22,6 @@ in { }) ]; - /* - Allow dream2nix to detect if a given directory contains a project - which can be translated with this translator. - Usually this can be done by checking for the existence of specific - file names or file endings. - - Alternatively a fully featured discoverer can be implemented under - `src/subsystems/{subsystem}/discoverers`. - This is recommended if more complex project structures need to be - discovered like, for example, workspace projects spanning over multiple - sub-directories - - If a fully featured discoverer exists, do not define `discoverProject`. - */ - discoverProject = tree: - (l.pathExists "${tree.fullPath}/composer.json") - && (l.pathExists "${tree.fullPath}/composer.lock"); - # translate from a given source and a project specification to a dream-lock. translate = { translatorName, @@ -189,9 +171,20 @@ in { }; doReplace = pkg: l.foldl replace pkg packages; doProvide = pkg: l.foldl provide pkg packages; - resolve = pkg: doProvide (doReplace pkg); + dropMissing = pkgs: let + doDropMissing = pkg: + pkg + // { + require = + l.filterAttrs + (name: semver: l.any (pkg: (pkg.name == name) && (satisfiesSemver pkg.version semver)) pkgs) + (getDependencies pkg); + }; + in + map doDropMissing pkgs; + resolve = pkg: (doProvide (doReplace pkg)); in - map resolve packages; + dropMissing (map resolve packages); # toplevel php semver phpSemver = composerJson.require."php" or "*"; @@ -204,7 +197,13 @@ in { map (l.strings.removePrefix "ext-") (l.lists.unique extensions); # get dependencies - getDependencies = pkg: (pkg.require or {}); + getDependencies = pkg: + l.mapAttrs + (name: version: + if version == "self.version" + then pkg.version + else version) + (pkg.require or {}); # resolve semvers into exact versions pinPackages = pkgs: let @@ -246,7 +245,7 @@ in { }; # name of the default package - defaultPackage = composerJson.name; + defaultPackage = project.name; /* List the package candidates which should be exposed to the user. @@ -254,7 +253,7 @@ in { Users will not be interested in all individual dependencies. */ exportedPackages = { - "${defaultPackage}" = composerJson.version or "0.0.0"; + "${defaultPackage}" = composerJson.version or "unknown"; }; /* @@ -276,7 +275,7 @@ in { ( if noDev then {} - else composerJson.require-dev + else composerJson.require-dev or {} ) // composerJson.require; } diff --git a/src/subsystems/php/utils.nix b/src/subsystems/php/utils.nix index b899b4b0e2..4634884dda 100644 --- a/src/subsystems/php/utils.nix +++ b/src/subsystems/php/utils.nix @@ -1,10 +1,8 @@ -{ - utils, - lib, - ... -}: let +{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 @@ -35,7 +33,7 @@ then "^${l.head m}" else c; removeV = c: let - m = l.match "^(.)v([[:d:]]+[.].*)$" c; + m = l.match "^(.)*v([[:d:]]+[.].*)$" c; in if m != null && l.length m > 0 then l.concatStrings m @@ -43,24 +41,30 @@ cleanConstraint = removeV (wildcard (tilde (removeSuffix constraint))); cleanVersion = removeX (l.removePrefix "v" (removeSuffix version)); in - (version == constraint) - || ( - utils.satisfiesSemver - cleanVersion - cleanConstraint - ); + (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 - trim = s: l.head (l.match "^[[:space:]]*([^[:space:]]*)[[:space:]]*$" s); 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 (satisfiesSemverSingle version) (splitAlternatives 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 @@ -68,6 +72,6 @@ in { satisfies = v: c: (v == "") || (v == "*") || (satisfiesSemverSingle v c); in l.any - (c: l.any (v: satisfies v c) (splitAlternatives multiversion)) + (c: l.any (v: l.all (satisfies v) (splitConjunctives c)) (splitAlternatives multiversion)) (splitAlternatives constraint); }