From 5b36429530387ce85cc586788eca54693602dea7 Mon Sep 17 00:00:00 2001 From: adisbladis Date: Sat, 7 Sep 2024 15:12:24 +1200 Subject: [PATCH] workspace: Add loadConfig It's better to expose config loading as it's own function, and to to make loaded config settings loaded overrideable. --- ...ace.loadConfig.testNo-binary-no-build.json | 6 ++ .../workspace.loadConfig.testNo-binary.json | 6 ++ ...onfig.testNo-build-no-binary-packages.json | 10 ++ .../workspace.loadConfig.testNo-build.json | 6 ++ .../workspace.loadConfig.testTrivial.json | 6 ++ .../workspace.loadConfig.testWorkspace.json | 6 ++ ...orkspace.loadConfig.testWorkspaceFlat.json | 6 ++ lib/lock1.nix | 63 +++-------- lib/test.nix | 6 +- lib/test_lock1.nix | 10 +- lib/test_workspace.nix | 33 +++++- lib/testutil.nix | 11 ++ lib/workspace.nix | 100 +++++++++++++++++- 13 files changed, 207 insertions(+), 62 deletions(-) create mode 100644 lib/expected/workspace.loadConfig.testNo-binary-no-build.json create mode 100644 lib/expected/workspace.loadConfig.testNo-binary.json create mode 100644 lib/expected/workspace.loadConfig.testNo-build-no-binary-packages.json create mode 100644 lib/expected/workspace.loadConfig.testNo-build.json create mode 100644 lib/expected/workspace.loadConfig.testTrivial.json create mode 100644 lib/expected/workspace.loadConfig.testWorkspace.json create mode 100644 lib/expected/workspace.loadConfig.testWorkspaceFlat.json create mode 100644 lib/testutil.nix diff --git a/lib/expected/workspace.loadConfig.testNo-binary-no-build.json b/lib/expected/workspace.loadConfig.testNo-binary-no-build.json new file mode 100644 index 0000000..ebd1acb --- /dev/null +++ b/lib/expected/workspace.loadConfig.testNo-binary-no-build.json @@ -0,0 +1,6 @@ +{ + "no-binary": true, + "no-binary-package": [], + "no-build": true, + "no-build-package": [] +} diff --git a/lib/expected/workspace.loadConfig.testNo-binary.json b/lib/expected/workspace.loadConfig.testNo-binary.json new file mode 100644 index 0000000..fd723a1 --- /dev/null +++ b/lib/expected/workspace.loadConfig.testNo-binary.json @@ -0,0 +1,6 @@ +{ + "no-binary": true, + "no-binary-package": [], + "no-build": false, + "no-build-package": [] +} diff --git a/lib/expected/workspace.loadConfig.testNo-build-no-binary-packages.json b/lib/expected/workspace.loadConfig.testNo-build-no-binary-packages.json new file mode 100644 index 0000000..e4f1fd7 --- /dev/null +++ b/lib/expected/workspace.loadConfig.testNo-build-no-binary-packages.json @@ -0,0 +1,10 @@ +{ + "no-binary": false, + "no-binary-package": [ + "urllib3" + ], + "no-build": false, + "no-build-package": [ + "arpeggio" + ] +} diff --git a/lib/expected/workspace.loadConfig.testNo-build.json b/lib/expected/workspace.loadConfig.testNo-build.json new file mode 100644 index 0000000..587d9b8 --- /dev/null +++ b/lib/expected/workspace.loadConfig.testNo-build.json @@ -0,0 +1,6 @@ +{ + "no-binary": false, + "no-binary-package": [], + "no-build": true, + "no-build-package": [] +} diff --git a/lib/expected/workspace.loadConfig.testTrivial.json b/lib/expected/workspace.loadConfig.testTrivial.json new file mode 100644 index 0000000..a87408b --- /dev/null +++ b/lib/expected/workspace.loadConfig.testTrivial.json @@ -0,0 +1,6 @@ +{ + "no-binary": false, + "no-binary-package": [], + "no-build": false, + "no-build-package": [] +} diff --git a/lib/expected/workspace.loadConfig.testWorkspace.json b/lib/expected/workspace.loadConfig.testWorkspace.json new file mode 100644 index 0000000..a87408b --- /dev/null +++ b/lib/expected/workspace.loadConfig.testWorkspace.json @@ -0,0 +1,6 @@ +{ + "no-binary": false, + "no-binary-package": [], + "no-build": false, + "no-build-package": [] +} diff --git a/lib/expected/workspace.loadConfig.testWorkspaceFlat.json b/lib/expected/workspace.loadConfig.testWorkspaceFlat.json new file mode 100644 index 0000000..a87408b --- /dev/null +++ b/lib/expected/workspace.loadConfig.testWorkspaceFlat.json @@ -0,0 +1,6 @@ +{ + "no-binary": false, + "no-binary-package": [], + "no-build": false, + "no-build-package": [] +} diff --git a/lib/lock1.nix b/lib/lock1.nix index b03e5ba..a95b8f2 100644 --- a/lib/lock1.nix +++ b/lib/lock1.nix @@ -35,7 +35,6 @@ let hasPrefix intersectLists assertMsg - foldl' ; in @@ -226,50 +225,17 @@ fix (self: { workspaceRoot, # deadnix: skip sourcePreference, + # tool.uv settings + config, }@wsargs: let - # All workspace pyproject.toml files (including a virtual workspace root) as a list - pyprojects' = map (project: project.pyproject) (attrValues projects) ++ [ pyproject ]; - - # implement https://docs.astral.sh/uv/reference/settings/#no-binary-package and no-build merging of multiple projects in a workspace is set addition - no-binary-packages = unique ( - concatMap (pyproject: pyproject.tool.uv.no-binary-package or [ ]) pyprojects' - ); - no-build-packages = unique ( - concatMap (pyproject: pyproject.tool.uv.no-build-package or [ ]) pyprojects' - ); - unbuildable-packages = intersectLists no-binary-packages no-build-packages; - - no-build = foldl' ( - acc: pyproject: - ( - if pyproject ? tool.uv.no-build then - ( - if acc != null && pyproject.tool.uv.no-build != acc then - (throw "Got conflicting values for tool.uv.no-build") - else - pyproject.tool.uv.no-build - ) - else - acc - ) - ) null pyprojects'; - - no-binary = foldl' ( - acc: pyproject: - ( - if pyproject ? tool.uv.no-binary then - ( - if acc != null && pyproject.tool.uv.no-binary != acc then - (throw "Got conflicting values for tool.uv.no-binary") - else - pyproject.tool.uv.no-binary - ) - else - acc - ) - ) null pyprojects'; - + inherit (config) + no-binary + no-build + no-binary-package + no-build-package + ; + unbuildable-packages = intersectLists no-binary-package no-build-package; in # Parsed uv.lock package package: @@ -292,7 +258,8 @@ fix (self: { # List of parsed wheels wheelFiles = map (whl: whl.file') package.wheels; - localProject = if projects ? package.name then + localProject = + if projects ? package.name then projects.${package.name} else pyproject-nix.lib.project.loadUVPyproject { @@ -327,9 +294,9 @@ fix (self: { true else if no-binary != null && no-binary then false - else if elem package.name no-binary-packages then + else if elem package.name no-binary-package then false - else if elem package.name no-build-packages then + else if elem package.name no-build-package then true else if sourcePreference == "sdist" then false @@ -382,9 +349,9 @@ fix (self: { assert assertMsg ( format == "sdist" -> no-build != null -> !no-build ) "Package source for '${package.name}' was derived as sdist, in tool.uv.no-build is set to true"; - assert assertMsg (format == "pyproject" -> !elem package.name no-build-packages) + assert assertMsg (format == "pyproject" -> !elem package.name no-build-package) "Package source for '${package.name}' was derived as sdist, but was present in tool.uv.no-build-package"; - assert assertMsg (format == "wheel" -> !elem package.name no-binary-packages) + assert assertMsg (format == "wheel" -> !elem package.name no-binary-package) "Package source for '${package.name}' was derived as wheel, but was present in tool.uv.no-binary-package"; if (isProject || isDirectory || isVirtual) then buildPythonPackage (localProject.renderers.buildPythonPackage { inherit python environ; }) diff --git a/lib/test.nix b/lib/test.nix index 12787f7..5b290c3 100644 --- a/lib/test.nix +++ b/lib/test.nix @@ -10,14 +10,10 @@ let fix mapAttrs mapAttrs' - toUpper - substring - stringLength length attrNames ; - - capitalise = s: toUpper (substring 0 1 s) + (substring 1 (stringLength s) s); + inherit (import ./testutil.nix { inherit lib; }) capitalise; callTest = path: import path (uv2nix // { inherit pkgs lib pyproject-nix; }); diff --git a/lib/test_lock1.nix b/lib/test_lock1.nix index e211f9e..b6917b8 100644 --- a/lib/test_lock1.nix +++ b/lib/test_lock1.nix @@ -1,5 +1,6 @@ { lock1, + workspace, lib, pkgs, pyproject-nix, @@ -13,9 +14,6 @@ let mapAttrs' importTOML nameValuePair - toUpper - substring - stringLength ; inherit (pyproject-nix.lib) pep508 pep621; inherit (builtins) baseNameOf; @@ -53,7 +51,7 @@ let findFirstPkg = name: findFirst (package: package.name == name) (throw "Not found: ${name}"); - capitalise = s: toUpper (substring 0 1 s) + (substring 1 (stringLength s) s); + inherit (import ./testutil.nix { inherit lib; }) capitalise; in @@ -99,6 +97,10 @@ in inherit workspaceRoot environ sourcePreference; projects = lib.filterAttrs (n: _: n == projectName) projects; inherit (projects.${projectName}) pyproject; + # Note: This doesn't support workspaces properly because we simply call loadConfig with the one workspace + # It's sufficient for mkPackage tests regardless. + config = workspace.loadConfig [ projects.${projectName}.pyproject ]; + # config = workspace.loadConfig }; in depName: diff --git a/lib/test_workspace.nix b/lib/test_workspace.nix index 34a424c..14e210c 100644 --- a/lib/test_workspace.nix +++ b/lib/test_workspace.nix @@ -7,6 +7,19 @@ let inherit (lib) nameValuePair listToAttrs; + inherit (import ./testutil.nix { inherit lib; }) capitalise; + + # Test fixture workspaces + workspaces = { + trivial = ./fixtures/trivial; + workspace = ./fixtures/workspace; + workspaceFlat = ./fixtures/workspace-flat; + no-build-no-binary-packages = ./fixtures/no-build-no-binary-packages; + no-build = ./fixtures/no-build; + no-binary = ./fixtures/no-binary; + no-binary-no-build = ./fixtures/no-binary-no-build; + }; + in { discoverWorkspace = @@ -17,18 +30,32 @@ in }; in { - testImplicitWorkspace = test ./fixtures/trivial [ "/" ]; - testWorkspace = test ./fixtures/workspace [ + testImplicitWorkspace = test workspaces.trivial [ "/" ]; + testWorkspace = test workspaces.workspace [ "/packages/workspace-package" "/" ]; - testWorkspaceFlat = test ./fixtures/workspace-flat [ + testWorkspaceFlat = test workspaces.workspaceFlat [ "/packages/pkg-a" "/packages/pkg-b" ]; testWorkspaceExcluded = test ./fixtures/workspace-with-excluded [ "/packages/included-package" ]; }; + loadConfig = lib.mapAttrs' ( + name': root: + let + name = "test${capitalise name'}"; + members = workspace.discoverWorkspace { workspaceRoot = root; }; + pyprojects = map (_m: lib.importTOML (root + "/pyproject.toml")) members; + config = workspace.loadConfig pyprojects; + in + nameValuePair name { + expr = config; + expected = lib.importJSON ./expected/workspace.loadConfig.${name}.json; + } + ) workspaces; + loadWorkspace.mkOverlay = let mkTest = diff --git a/lib/testutil.nix b/lib/testutil.nix new file mode 100644 index 0000000..ba16a79 --- /dev/null +++ b/lib/testutil.nix @@ -0,0 +1,11 @@ +{ lib }: +let + inherit (lib) + toUpper + substring + stringLength + ; +in +{ + capitalise = s: toUpper (substring 0 1 s) + (substring 1 (stringLength s) s); +} diff --git a/lib/workspace.nix b/lib/workspace.nix index 4c7f05e..c37b7df 100644 --- a/lib/workspace.nix +++ b/lib/workspace.nix @@ -25,6 +25,12 @@ let head attrNames all + unique + foldl' + isPath + isAttrs + attrValues + assertMsg ; inherit (builtins) readDir; inherit (pyproject-nix.lib.project) loadUVPyproject; # Note: Maybe we want a loader that will just "remap-all-the-things" into standard attributes? @@ -56,7 +62,14 @@ fix (self: { to create a Nixpkgs Python packageOverrides overlay */ loadWorkspace = - { workspaceRoot }: + { + # Workspace root as a path + workspaceRoot, + # Config overrides for settings automatically inferred by loadConfig + config ? { }, + }: + assert isPath workspaceRoot; + assert isAttrs config; let pyproject = importTOML (workspaceRoot + "/pyproject.toml"); uvLock = lock1.parseLock (importTOML (workspaceRoot + "/uv.lock")); @@ -85,8 +98,30 @@ fix (self: { # Bootstrap resolver from top-level workspace projects topLevelDependencies = map pep508.parseString (attrNames workspaceProjects); + config' = + # Load supported tool.uv settings + (self.loadConfig ( + # Extract pyproject.toml from loaded projects + (map (project: project.pyproject) (attrValues workspaceProjects)) + # If workspace root is a virtual root it wasn't discovered as a member directory + # but config should also be loaded from a virtual root + ++ optional (!(pyproject ? project)) pyproject + )) + // + # Merge with overriden config + config; + in + assert assertMsg ( + !(config'.no-binary && config'.no-build) + ) "Both tool.uv.no-build and tool.uv.no-binary are set to true, making the workspace unbuildable"; { + /* + Workspace config as loaded by loadConfig + . + */ + config = config'; + /* Generate a Nixpkgs Python overlay from uv workspace. @@ -99,7 +134,13 @@ fix (self: { # - sdist # # See FAQ for more information. - sourcePreference, + sourcePreference ? + if config'.no-binary then + "sdist" + else if config'.no-build then + "wheel" + else + throw "No sourcePreference was passed, and could not be automatically inferred from workspace config", # PEP-508 environment customisations. # Example: { platform_release = "5.10.65"; } environ ? { }, @@ -117,6 +158,7 @@ fix (self: { mkPackage = lock1.mkPackage { projects = workspaceProjects; environ = environ'; + config = config'; inherit workspaceRoot sourcePreference pyproject; }; @@ -139,6 +181,60 @@ fix (self: { inherit topLevelDependencies; }; + /* + Load supported configuration from workspace + + Supports: + - tool.uv.no-binary + - tool.uv.no-build + - tool.uv.no-binary-packages + - tool.uv.no-build-packages + */ + loadConfig = + # List of imported (lib.importTOML) pyproject.toml files from workspace from which to load config + pyprojects: + let + no-build' = foldl' ( + acc: pyproject: + ( + if pyproject ? tool.uv.no-build then + ( + if acc != null && pyproject.tool.uv.no-build != acc then + (throw "Got conflicting values for tool.uv.no-build") + else + pyproject.tool.uv.no-build + ) + else + acc + ) + ) null pyprojects; + + no-binary' = foldl' ( + acc: pyproject: + ( + if pyproject ? tool.uv.no-binary then + ( + if acc != null && pyproject.tool.uv.no-binary != acc then + (throw "Got conflicting values for tool.uv.no-binary") + else + pyproject.tool.uv.no-binary + ) + else + acc + ) + ) null pyprojects; + in + { + no-build = if no-build' != null then no-build' else false; + no-binary = if no-binary' != null then no-binary' else false; + no-binary-package = unique ( + concatMap (pyproject: pyproject.tool.uv.no-binary-package or [ ]) pyprojects + ); + no-build-package = unique ( + concatMap (pyproject: pyproject.tool.uv.no-build-package or [ ]) pyprojects + ); + }; + /* Discover workspace member directories from a workspace root. Returns a list of strings relative to the workspace root.