Merge pull request #286 from tinybeachthor/php-builder

php: builder
This commit is contained in:
DavHau 2022-09-13 09:39:03 +02:00 committed by GitHub
commit c2ebb96bf2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 417 additions and 58 deletions

View File

@ -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.

View File

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

View File

@ -9,6 +9,7 @@
rust = "build-rust-package";
nodejs = "granular";
python = "simple-builder";
php = "simple";
};
# TODO

View File

@ -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 <<EOF >> $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;
};
}

View File

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

View File

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

View File

@ -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 = "([=><!~^]+)";
version = "((0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)|(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)|(0|[1-9][0-9]*)){0,1}([.x*]*)(-((0|[1-9][0-9]*|[0-9]*[a-zA-Z-][0-9a-zA-Z-]*)(\\.(0|[1-9][0-9]*|[0-9]*[a-zA-Z-][0-9a-zA-Z-]*))*)){0,1}(\\+([0-9a-zA-Z-]+(\\.[0-9a-zA-Z-]+)*)){0,1}";
};
reLengths = {
operators = 1;
version = 16;
};
parseConstraint = constraintStr: let
# The common prefix operators
mPre = l.match "${re.operators} *${re.version}" constraintStr;
# There is an upper bound to the operator (this implementation is a bit hacky)
mUpperBound =
l.match "${re.operators} *${re.version} *< *${re.version}" constraintStr;
# There is also an infix operator to match ranges
mIn = l.match "${re.version} - *${re.version}" constraintStr;
# There is no operators
mNone = l.match "${re.version}" constraintStr;
in (
if mPre != null
then {
ops.t = l.elemAt mPre 0;
v = orBlank (l.elemAt mPre reLengths.operators);
}
# Infix operators are range matches
else if mIn != null
then {
ops = {
t = "-";
l = ">=";
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;
}

View File

@ -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.

View File

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

View File

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