diff --git a/dev/checks.nix b/dev/checks.nix index 7463f93..2f57ff5 100644 --- a/dev/checks.nix +++ b/dev/checks.nix @@ -83,7 +83,41 @@ let let mkCheck = mkCheck' sourcePreference; in - mapAttrs' (name: v: nameValuePair "${name}-pref-${sourcePreference}" v) { + { + editable-workspace = + let + workspaceRoot = ../lib/fixtures/workspace; + ws = uv2nix.workspace.loadWorkspace { inherit workspaceRoot; }; + overlay = ws.mkOverlay { + sourcePreference = "wheel"; # We're testing editable support, wheels are fine. + }; + editableOverlay = ws.mkEditableOverlay { + root = "$NIX_BUILD_TOP"; + }; + + python = pkgs.python312.override { + self = python; + packageOverrides = lib.composeExtensions overlay editableOverlay; + }; + + pythonEnv = python.withPackages (ps: [ ps.workspace ]); + + in + pkgs.runCommand "editable-workspace-test" + { + nativeBuildInputs = [ pythonEnv ]; + } + '' + cp -r ${workspaceRoot}/* . + chmod +w .* + test "$(python -c 'import workspace_package; print(workspace_package.hello())')" = "Hello from workspace-package!" + substituteInPlace ./packages/workspace-package/src/workspace_package/__init__.py --replace-fail workspace-package mutable-package + test "$(python -c 'import workspace_package; print(workspace_package.hello())')" = "Hello from mutable-package!" + touch $out + ''; + + } + // mapAttrs' (name: v: nameValuePair "${name}-pref-${sourcePreference}" v) { trivial = mkCheck { root = ../lib/fixtures/trivial; packages = ps: [ ps."trivial" ]; diff --git a/flake.lock b/flake.lock index 8be6b36..a3a891c 100644 --- a/flake.lock +++ b/flake.lock @@ -121,16 +121,16 @@ }, "nixpkgs": { "locked": { - "lastModified": 1725103162, - "narHash": "sha256-Ym04C5+qovuQDYL/rKWSR+WESseQBbNAe5DsXNx5trY=", - "owner": "nixos", + "lastModified": 1725401364, + "narHash": "sha256-J5jaUCMWdJjtu7v6k1EalUFQuDvAu0/HJ5/o1mAxuk8=", + "owner": "adisbladis", "repo": "nixpkgs", - "rev": "12228ff1752d7b7624a54e9c1af4b222b3c1073b", + "rev": "41611f8ee5c040cc4f0910669075340507f0b6ed", "type": "github" }, "original": { - "owner": "nixos", - "ref": "nixos-unstable", + "owner": "adisbladis", + "ref": "python-editable", "repo": "nixpkgs", "type": "github" } @@ -151,15 +151,16 @@ ] }, "locked": { - "lastModified": 1725356581, - "narHash": "sha256-mCYjrOn8Mv0HgIY9tQx+NZA8xK2TQic76VNpSo8EPrY=", - "owner": "adisbladis", + "lastModified": 1725410671, + "narHash": "sha256-s4t5ZR/YgzXA/TNv8qayM7Hez9ASStEeii5ySyZ+uyw=", + "owner": "nix-community", "repo": "pyproject.nix", - "rev": "424e96048451c58c36471c9b78860ed1c4d539dd", + "rev": "f70611faf95f683e50da2ce4ac1257f9256a3d5f", "type": "github" }, "original": { - "owner": "adisbladis", + "owner": "nix-community", + "ref": "editable", "repo": "pyproject.nix", "type": "github" } diff --git a/flake.nix b/flake.nix index c9a0190..ca42283 100644 --- a/flake.nix +++ b/flake.nix @@ -2,7 +2,7 @@ description = "Uv2nix"; inputs = { - nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable"; + nixpkgs.url = "github:adisbladis/nixpkgs/python-editable"; flake-parts.url = "github:hercules-ci/flake-parts"; flake-parts.inputs.nixpkgs-lib.follows = "nixpkgs"; @@ -16,7 +16,7 @@ nixdoc.url = "github:nix-community/nixdoc"; nixdoc.inputs.nixpkgs.follows = "nixpkgs"; - pyproject-nix.url = "github:adisbladis/pyproject.nix"; + pyproject-nix.url = "github:nix-community/pyproject.nix/editable"; pyproject-nix.inputs.nixpkgs.follows = "nixpkgs"; pyproject-nix.inputs.nix-github-actions.follows = "nix-github-actions"; pyproject-nix.inputs.mdbook-nixdoc.follows = "mdbook-nixdoc"; diff --git a/lib/fixtures/workspace/uv.lock b/lib/fixtures/workspace/uv.lock index 8bc57bb..4f01609 100644 --- a/lib/fixtures/workspace/uv.lock +++ b/lib/fixtures/workspace/uv.lock @@ -1,9 +1,6 @@ version = 1 requires-python = ">=3.12" -[options] -exclude-newer = "2024-08-21T10:06:00Z" - [manifest] members = [ "workspace", diff --git a/lib/lock1.nix b/lib/lock1.nix index a95b8f2..e398320 100644 --- a/lib/lock1.nix +++ b/lib/lock1.nix @@ -35,6 +35,7 @@ let hasPrefix intersectLists assertMsg + isString ; in @@ -258,20 +259,17 @@ fix (self: { # List of parsed wheels wheelFiles = map (whl: whl.file') package.wheels; + # Local projects + localPath = + "/" + + source.editable or source.directory or source.virtual + or (throw "Not a project path: ${toJSON source}"); localProject = if projects ? package.name then projects.${package.name} else pyproject-nix.lib.project.loadUVPyproject { - projectRoot = - if isProject then - workspaceRoot + "/${source.editable}" - else if isDirectory then - workspaceRoot + "/${source.directory}" - else if isVirtual then - workspaceRoot + "/${source.virtual}" - else - throw "Not a project path: ${toJSON source}"; + projectRoot = workspaceRoot + localPath; }; in @@ -286,7 +284,11 @@ fix (self: { stdenv, autoPatchelfHook, pythonManylinuxPackages, + mkPythonEditablePackage, + # Source preference (sdist or wheel) sourcePreference ? wsargs.sourcePreference, + # Editable root as a string + editableRoot ? null, }: let preferWheel = @@ -354,7 +356,23 @@ fix (self: { 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; }) + # Package is not editable, render buildPythonPackage + if editableRoot == null then + buildPythonPackage (localProject.renderers.buildPythonPackage { inherit python environ; }) + # Package is editable, render mkPythonEditablePackage + else + assert isString editableRoot; + mkPythonEditablePackage ( + localProject.renderers.mkPythonEditablePackage { + inherit python environ; + # Prefer src style layout if available, otherwise use project root as editable root. + root = + if lib.pathExists (workspaceRoot + localPath + "/src") then + editableRoot + localPath + "/src" + else + editableRoot + localPath; + } + ) else buildPythonPackage ( { diff --git a/lib/workspace.nix b/lib/workspace.nix index c37b7df..cef8e67 100644 --- a/lib/workspace.nix +++ b/lib/workspace.nix @@ -31,6 +31,7 @@ let isAttrs attrValues assertMsg + nameValuePair ; 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? @@ -178,6 +179,28 @@ fix (self: { assert all (spec: pep440.comparators.${spec.op} pythonVersion spec.version) uvLock.requires-python; mapAttrs (_: pkg: callPackage (mkPackage pkg) { }) resolved; + mkEditableOverlay = + let + isLocalPackage = _package: false; + workspaceProjects' = attrNames workspaceProjects; + localProjects = map (package: package.name) (filter isLocalPackage uvLock.package); + allLocal = unique (workspaceProjects' ++ localProjects); + in + { + # Editable root as a string. + root ? (toString workspaceRoot), + # Workspace members to make editable as a list of strings. Defaults to all local projects. + members ? allLocal, + }: + _final: prev: + let + # Filter any local packages that might be deactivated by markers or other filtration mechanisms. + activeMembers = filter (name: !prev ? name) members; + in + lib.listToAttrs ( + map (name: nameValuePair name (prev.${name}.override { editableRoot = root; })) activeMembers + ); + inherit topLevelDependencies; }; diff --git a/templates/app/flake.nix b/templates/app/flake.nix index e714a9e..14d80f9 100644 --- a/templates/app/flake.nix +++ b/templates/app/flake.nix @@ -20,7 +20,6 @@ overlay = let # Create overlay from workspace. - overlay' = workspace.mkOverlay { # Prefer prebuilt binary wheels as a package source. # Sdists are less likely to "just work" because of the metadata missing from uv.lock.