Port php to dream2nix modules (#623)

* init: composer

* initial attempt to translating composer lock

* fix(v1/php): use simpleTranslate2 instead of simpleTranslate

* chore: rebasing

* init: granular module

* lib/internal/simpleTranslate2: revert accidental changes

* php: fix php-granular builder module

* php-compser-lock: cleanup

---------

Co-authored-by: asrar <aszenz@gmail.com>
This commit is contained in:
DavHau 2023-08-29 17:47:13 +02:00 committed by GitHub
parent 9b721a5cd3
commit cf0bd2e99a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 947 additions and 0 deletions

View File

@ -0,0 +1,31 @@
{
lib,
config,
dream2nix,
...
}: {
imports = [
dream2nix.modules.drv-parts.php-composer-lock
dream2nix.modules.drv-parts.php-granular
];
mkDerivation = {
src = config.deps.fetchFromGitHub {
owner = "Gipetto";
repo = "CowSay";
rev = config.version;
sha256 = "sha256-jriyCzmvT2pPeNQskibBg0Bsh+h64cAEO+yOOfX2wbA=";
};
};
deps = {nixpkgs, ...}: {
inherit
(nixpkgs)
fetchFromGitHub
stdenv
;
};
name = "cowsay";
version = "1.2.0";
}

197
lib/internal/php-semver.nix Normal file
View File

@ -0,0 +1,197 @@
{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);
upper = builtins.toString (l.toInt (l.head ver) + 1);
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-9][0-9]*)|(0|[1-9][0-9]*)[.](0|[1-9][0-9]*)|(0|[1-9][0-9]*))?(-((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-9a-zA-Z-]+(\\.[0-9a-zA-Z-]+)*))?";
};
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''
);
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;
# remove v from version strings: ^v1.2.3 -> ^1.2.3
# remove branch suffix: ^1.2.x-dev -> ^1.2
satisfiesSingle = version: constraint: let
removeStability = c: let
m = l.match "^(.*)[@][[:alpha:]]+$" c;
in
if m != null && l.length m >= 0
then l.head m
else c;
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}.0"
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 (removeStability (l.removePrefix "dev-" constraint))));
cleanVersion = l.removePrefix "v" (wildcard (removeSuffix version));
in
(l.elem (removeStability constraint) ["" "*"])
|| (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.filter (x: 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);
}

View File

@ -0,0 +1,51 @@
{
config,
lib,
...
}: let
l = lib // builtins;
cfg = config.php-composer-lock;
dreamLockUtils = import ../../../lib/internal/dreamLockUtils.nix {inherit lib;};
nodejsUtils = import ../../../lib/internal/nodejsUtils.nix {inherit lib parseSpdxId;};
parseSpdxId = import ../../../lib/internal/parseSpdxId.nix {inherit lib;};
prepareSourceTree = import ../../../lib/internal/prepareSourceTree.nix {inherit lib;};
simpleTranslate2 = import ../../../lib/internal/simpleTranslate2.nix {inherit lib;};
translate = import ./translate.nix {
inherit lib dreamLockUtils nodejsUtils parseSpdxId simpleTranslate2;
};
dreamLock = translate {
projectName = cfg.composerJson.name;
projectRelPath = "";
source = cfg.source;
tree = prepareSourceTree {source = cfg.source;};
noDev = ! cfg.withDevDependencies;
# php = "unknown";
inherit (cfg) composerJson composerLock;
};
in {
imports = [
./interface.nix
];
# declare external dependencies
deps = {nixpkgs, ...}: {
inherit
(nixpkgs)
fetchgit
fetchurl
nix
runCommandLocal
;
};
php-composer-lock = {
inherit dreamLock;
composerJson = l.fromJSON (l.readFile cfg.composerJsonFile);
composerLock =
if cfg.composerLockFile != null
then l.fromJSON (l.readFile cfg.composerLockFile)
else lib.mkDefault {};
};
}

View File

@ -0,0 +1,53 @@
{
config,
options,
lib,
...
}: let
l = lib // builtins;
t = l.types;
cfg = config.php-composer-lock;
in {
options.php-composer-lock = l.mapAttrs (_: l.mkOption) {
dreamLock = {
type = t.attrs;
internal = true;
description = "The content of the dream2nix generated lock file";
};
composerJsonFile = {
type = t.path;
description = ''
The composer.json file to use.
'';
default = cfg.source + "/composer.json";
};
composerJson = {
type = t.attrs;
description = "The content of the composer.json";
};
composerLockFile = {
type = t.nullOr t.path;
description = ''
The composer.lock file to use.
'';
default = cfg.source + "/composer.lock";
};
composerLock = {
type = t.attrs;
description = "The content of the composer.lock";
};
source = {
type = t.either t.path t.package;
description = "Source of the package";
default = config.mkDerivation.src;
};
withDevDependencies = {
type = t.bool;
default = true;
description = ''
Whether to include development dependencies.
Usually it's a bad idea to disable this, as development dependencies can contain important build time dependencies.
'';
};
};
}

View File

@ -0,0 +1,260 @@
{
lib,
nodejsUtils,
dreamLockUtils,
simpleTranslate2,
...
}: let
l = lib // builtins;
# translate from a given source and a project specification to a dream-lock.
translate = {
projectName,
projectRelPath,
composerLock,
composerJson,
tree,
noDev,
...
}: let
inherit
(import ../../../lib/internal/php-semver.nix {inherit lib;})
satisfies
multiSatisfies
;
# get the root source and project source
rootSource = tree.fullPath;
projectTree = tree.getNodeFromPath projectRelPath;
composerJson = (projectTree.getNodeFromPath "composer.json").jsonContent;
composerLock = (projectTree.getNodeFromPath "composer.lock").jsonContent;
# 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;
composerPluginApiSemver = l.listToAttrs (l.flatten (map
(
pkg: let
requires = getRequire pkg;
in
l.optional (requires ? "composer-plugin-api")
{
name = "${pkg.name}@${pkg.version}";
value = requires."composer-plugin-api";
}
)
packages));
# 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 = projectName;
version = composerJson.version or "unknown";
source = {
type = "path";
path = rootSource;
};
require =
(l.optionalAttrs (!noDev) (composerJson.require-dev or {}))
// (composerJson.require or {});
};
# all the packages
packages =
# 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
apply = pkg: dep: candidates: let
original = getRequire pkg;
applied =
l.filterAttrs
(
name: semver:
!((candidates ? "${name}") && (multiSatisfies candidates."${name}" semver))
)
original;
in
pkg
// {
require =
applied
// (
l.optionalAttrs
(applied != original)
{"${dep.name}" = "${dep.version}";}
);
};
dropMissing = pkgs: let
doDropMissing = pkg:
pkg
// {
require =
l.filterAttrs
(name: semver: l.any (pkg: (pkg.name == name) && (satisfies pkg.version semver)) pkgs)
(getRequire pkg);
};
in
map doDropMissing pkgs;
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
dropMissing (map (pkg: (doProvide (doReplace pkg))) packages);
# resolve semvers into exact versions
pinPackages = pkgs: let
clean = requires:
l.filterAttrs
(name: _:
!(l.elem name ["php" "composer-plugin-api" "composer-runtime-api"])
&& !(l.strings.hasPrefix "ext-" name))
requires;
doPin = name: semver:
(l.head
(l.filter (dep: satisfies dep.version semver)
(l.filter (dep: dep.name == name)
resolvedPackages)))
.version;
doPins = pkg:
pkg
// {
require = l.mapAttrs doPin (clean pkg.require);
};
in
map doPins pkgs;
in
simpleTranslate2
({objectsByKey, ...}: rec {
translatorName = "composer-lock";
# relative path of the project within the source tree.
location = projectRelPath;
# the name of the subsystem
subsystemName = "php";
# Extract subsystem specific attributes.
# The structure of this should be defined in:
# ./src/specifications/{subsystem}
subsystemAttrs = {
inherit noDev;
inherit phpSemver phpExtensions;
inherit composerPluginApiSemver;
};
# name of the default package
defaultPackage = toplevelPackage.name;
/*
List the package candidates which should be exposed to the user.
Only top-level packages should be listed here.
Users will not be interested in all individual dependencies.
*/
exportedPackages = {
"${defaultPackage}" = toplevelPackage.version;
};
/*
a list of raw package objects
If the upstream format is a deep attrset, this list should contain
a flattened representation of all entries.
*/
serializedRawObjects = pinPackages resolvedPackages;
/*
Define extractor functions which each extract one property from
a given raw object.
(Each rawObj comes from serializedRawObjects).
Extractors can access the fields extracted by other extractors
by accessing finalObj.
*/
extractors = {
name = rawObj: finalObj:
rawObj.name;
version = rawObj: finalObj:
rawObj.version;
dependencies = rawObj: finalObj:
l.attrsets.mapAttrsToList
(name: version: {inherit name version;})
(getRequire rawObj);
sourceSpec = rawObj: finalObj:
if rawObj ? "source" && rawObj.source.type == "path"
then {
inherit (rawObj.source) type path;
rootName = finalObj.name;
rootVersion = finalObj.version;
}
else if rawObj ? "source" && rawObj.source.type == "git"
then {
inherit (rawObj.source) type url;
rev = rawObj.source.reference;
submodules = false;
}
else if rawObj ? "dist" && rawObj.dist.type == "path"
then {
inherit (rawObj.dist) type;
path = rawObj.dist.url;
rootName = null;
rootVersion = null;
}
else
l.abort ''
Cannot find source for ${finalObj.name}@${finalObj.version},
rawObj: ${l.toJSON rawObj}
'';
};
/*
Optionally define extra extractors which will be used to key all
final objects, so objects can be accessed via:
`objectsByKey.${keyName}.${value}`
*/
keys = {
};
/*
Optionally add extra objects (list of `finalObj`) to be added to
the dream-lock.
*/
extraObjects = [
];
});
in
translate

View File

@ -0,0 +1,301 @@
{
config,
lib,
dream2nix,
...
}: let
l = lib // builtins;
cfg = config.php-granular;
dreamLock = config.php-composer-lock.dreamLock;
fetchDreamLockSources =
import ../../../lib/internal/fetchDreamLockSources.nix
{inherit lib;};
getDreamLockSource = import ../../../lib/internal/getDreamLockSource.nix {inherit lib;};
readDreamLock = import ../../../lib/internal/readDreamLock.nix {inherit lib;};
hashPath = import ../../../lib/internal/hashPath.nix {
inherit lib;
inherit (config.deps) runCommandLocal nix;
};
# fetchers
fetchers = {
git = import ../../../lib/internal/fetchers/git {
inherit hashPath;
inherit (config.deps) fetchgit;
};
path = import ../../../lib/internal/fetchers/path {
inherit hashPath;
};
};
dreamLockLoaded = readDreamLock {inherit dreamLock;};
dreamLockInterface = dreamLockLoaded.interface;
inherit (dreamLockInterface) defaultPackageName defaultPackageVersion;
fetchedSources = fetchDreamLockSources {
inherit defaultPackageName defaultPackageVersion;
inherit (dreamLockLoaded.lock) sources;
inherit fetchers;
};
getSource = getDreamLockSource fetchedSources;
inherit
(dreamLockInterface)
getDependencies # name: version: -> [ {name=; version=; } ]
# Attributes
subsystemAttrs # attrset
packageVersions
;
inherit (import ../../../lib/internal/php-semver.nix {inherit lib;}) satisfies;
# php with required extensions
php =
if satisfies config.deps.php81.version subsystemAttrs.phpSemver
then
config.deps.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 "${config.deps.php81.version}" from attribute "config.deps.php81".
'';
composer = php.packages.composer;
# packages to export
# packages =
# {default = packages.${defaultPackageName};}
# // (
# lib.mapAttrs
# (name: version: {
# "${version}" = allPackages.${name}.${version};
# })
# dreamLockInterface.packages
# );
# devShells =
# {default = devShells.${defaultPackageName};}
# // (
# l.mapAttrs
# (name: version: packages.${name}.${version}.devShell)
# dreamLockInterface.packages
# );
# Generates a derivation for a specific package name + version
commonModule = name: version: let
isTopLevel = dreamLockInterface.packages.name or null == version;
# name = l.strings.sanitizeDerivationName name;
dependencies = getDependencies name version;
repositories = let
transform = dep: let
intoRepository = name: version: root: {
type = "path";
url = "${root}/vendor/${name}";
options = {
versions = {
"${l.strings.toLower name}" = "${version}";
};
symlink = false;
};
};
getAllSubdependencies = deps: let
getSubdependencies = dep: let
subdeps = getDependencies dep.name dep.version;
in
l.flatten ([dep] ++ (getAllSubdependencies subdeps));
in
l.flatten (map getSubdependencies deps);
depRoot = cfg.deps."${dep.name}"."${dep.version}".public;
direct = intoRepository dep.name dep.version "${depRoot}/lib";
transitive =
map
(subdep: intoRepository subdep.name subdep.version "${depRoot}/lib/vendor/${dep.name}")
(getAllSubdependencies (getDependencies dep.name dep.version));
in
[direct] ++ transitive;
in
l.flatten (map transform dependencies);
repositoriesString =
l.toJSON
(repositories ++ [{packagist = false;}]);
dependenciesString = l.toJSON (l.listToAttrs (
map
(dep: {
name = l.strings.toLower dep.name;
value = dep.version;
})
(dependencies
++ l.optional (subsystemAttrs.composerPluginApiSemver ? "${name}@${version}")
{
name = "composer-plugin-api";
version = subsystemAttrs.composerPluginApiSemver."${name}@${version}";
})
));
versionString =
if version == "unknown"
then "0.0.0"
else version;
module = {config, ...}: {
imports = [
dream2nix.modules.drv-parts.mkDerivation
];
deps = {nixpkgs, ...}:
l.mapAttrs (_: l.mkDefault) {
inherit
(nixpkgs)
jq
mkShell
moreutils
php81
stdenv
;
};
public.devShell = import ./devShell.nix {
inherit
name
php
composer
;
pkg = config.public;
inherit (config.deps) mkShell;
};
php-granular = {
composerInstallFlags =
[
"--no-scripts"
"--no-plugins"
]
++ l.optional (subsystemAttrs.noDev || !isTopLevel) "--no-dev";
};
env = {
inherit dependenciesString repositoriesString;
};
mkDerivation = {
src = l.mkDefault (getSource name version);
nativeBuildInputs = with config.deps; [
jq
composer
moreutils
];
buildInputs = with config.deps;
[
php
composer
]
++ map (dep: cfg.deps."${dep.name}"."${dep.version}".public)
dependencies;
passAsFile = ["repositoriesString" "dependenciesString"];
unpackPhase = ''
runHook preUnpack
mkdir -p $out/lib/vendor/${name}
cd $out/lib/vendor/${name}
# copy source
cp -r ${config.mkDerivation.src}/* .
chmod -R +w .
# create composer.json if does not exist
if [ ! -f composer.json ]; then
echo "{}" > composer.json
fi
# save the original composer.json for reference
cp composer.json composer.json.orig
# set name & version
jq \
"(.name = \"${name}\") | \
(.version = \"${versionString}\") | \
(.extra.patches = {})" \
composer.json | sponge composer.json
runHook postUnpack
'';
configurePhase = ''
runHook preConfigure
# disable packagist, set path repositories
jq \
--slurpfile repositories $repositoriesStringPath \
--slurpfile dependencies $dependenciesStringPath \
"(.repositories = \$repositories[0]) | \
(.require = \$dependencies[0]) | \
(.\"require-dev\" = {})" \
composer.json | sponge composer.json
runHook postConfigure
'';
buildPhase = ''
runHook preBuild
# remove composer.lock if exists
rm -f composer.lock
# build
composer install ${l.strings.concatStringsSep " " config.php-granular.composerInstallFlags}
runHook postBuild
rm -rfv vendor/*/*/vendor
'';
installPhase = ''
runHook preInstall
BINS=$(jq -rcM "(.bin // [])[]" composer.json)
for bin in $BINS
do
mkdir -p $out/bin
pushd $out/bin
ln -s $out/lib/vendor/${name}/$bin
popd
done
runHook postInstall
'';
};
};
in
module;
in {
imports = [
./interface.nix
(commonModule defaultPackageName defaultPackageVersion)
];
php-granular.deps =
lib.mapAttrs
(name: versions:
lib.genAttrs
versions
(
version:
# the submodule for this dependency
{...}: {
imports = [(commonModule name version)];
inherit name version;
}
))
packageVersions;
}

View File

@ -0,0 +1,27 @@
{
name,
pkg,
mkShell,
php,
composer,
}:
mkShell {
buildInputs = [
php
composer
];
shellHook = let
vendorDir =
pkg.config.package-func.result.overrideAttrs
(_: {
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,27 @@
{
config,
dream2nix,
packageSets,
lib,
...
}: let
l = lib // builtins;
t = l.types;
in {
options.php-granular = l.mapAttrs (_: l.mkOption) {
deps = {
type = t.attrsOf (t.attrsOf (t.submodule {
imports = [
dream2nix.modules.drv-parts.core
dream2nix.modules.drv-parts.mkDerivation
./interface.nix
];
_module.args = {inherit dream2nix packageSets;};
}));
};
composerInstallFlags = {
type = t.listOf t.string;
default = [];
};
};
}