fastws-cli 0.0.22__tar.gz → 0.0.24__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {fastws_cli-0.0.22/fastws_cli.egg-info → fastws_cli-0.0.24}/PKG-INFO +12 -5
- {fastws_cli-0.0.22 → fastws_cli-0.0.24}/README.md +10 -4
- {fastws_cli-0.0.22 → fastws_cli-0.0.24}/fastws/__init__.py +3 -1
- {fastws_cli-0.0.22 → fastws_cli-0.0.24}/fastws/build_backend.py +22 -3
- {fastws_cli-0.0.22 → fastws_cli-0.0.24}/fastws/cargo.py +10 -5
- {fastws_cli-0.0.22 → fastws_cli-0.0.24}/fastws/core.py +36 -34
- {fastws_cli-0.0.22 → fastws_cli-0.0.24/fastws_cli.egg-info}/PKG-INFO +12 -5
- {fastws_cli-0.0.22 → fastws_cli-0.0.24}/fastws_cli.egg-info/SOURCES.txt +1 -0
- {fastws_cli-0.0.22 → fastws_cli-0.0.24}/fastws_cli.egg-info/requires.txt +1 -0
- {fastws_cli-0.0.22 → fastws_cli-0.0.24}/pyproject.toml +1 -0
- fastws_cli-0.0.24/tests/test_build_backend.py +26 -0
- {fastws_cli-0.0.22 → fastws_cli-0.0.24}/tests/test_sync.py +3 -3
- {fastws_cli-0.0.22 → fastws_cli-0.0.24}/LICENSE +0 -0
- {fastws_cli-0.0.22 → fastws_cli-0.0.24}/fastws/releases.py +0 -0
- {fastws_cli-0.0.22 → fastws_cli-0.0.24}/fastws_cli.egg-info/dependency_links.txt +0 -0
- {fastws_cli-0.0.22 → fastws_cli-0.0.24}/fastws_cli.egg-info/entry_points.txt +0 -0
- {fastws_cli-0.0.22 → fastws_cli-0.0.24}/fastws_cli.egg-info/top_level.txt +0 -0
- {fastws_cli-0.0.22 → fastws_cli-0.0.24}/setup.cfg +0 -0
- {fastws_cli-0.0.22 → fastws_cli-0.0.24}/tests/test_build.py +0 -0
- {fastws_cli-0.0.22 → fastws_cli-0.0.24}/tests/test_releases.py +0 -0
- {fastws_cli-0.0.22 → fastws_cli-0.0.24}/tests/test_setup.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: fastws-cli
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.24
|
|
4
4
|
Summary: Fast workspace tools for multi-repo management
|
|
5
5
|
Author: Jeremy Howard
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -14,6 +14,7 @@ Requires-Dist: fastcore>=2.2.33
|
|
|
14
14
|
Requires-Dist: fastgit>=0.1.2
|
|
15
15
|
Requires-Dist: ghapi>=2.0.6
|
|
16
16
|
Requires-Dist: packaging
|
|
17
|
+
Requires-Dist: maturin<2.0,>=1.9.4
|
|
17
18
|
Requires-Dist: tomli; python_version < "3.11"
|
|
18
19
|
Provides-Extra: dev
|
|
19
20
|
Requires-Dist: fastship; extra == "dev"
|
|
@@ -99,7 +100,7 @@ Keep `repos.txt` tracked in the workspace Git repo. Put personal additions in a
|
|
|
99
100
|
|
|
100
101
|
`--repos-file myrepos.txt` uses `myrepos-local.txt` alongside it. Either list may be absent. Removing a repo from the baseline does not delete its checkout. The next sync discovers an existing root checkout as a personal addition. External checkouts also remain on disk; add them to the local list to keep managing them.
|
|
101
102
|
|
|
102
|
-
The workspace `pyproject.toml` is local generated state. `ws-sync` creates it from the tracked `pyproject.tmpl` when available, or generates a minimal one, then maintains it while preserving personal settings except for the repo extras
|
|
103
|
+
The workspace `pyproject.toml` is local generated state. `ws-sync` creates it from the tracked `pyproject.tmpl` when available, or generates a minimal one, then maintains it while preserving personal settings except for the managed repo extras, workspace exclusions, and build-isolation list. The template supplies starting defaults only: later template edits do not overwrite existing configuration. Ignore these files in the workspace repo (not in its member repos):
|
|
103
104
|
|
|
104
105
|
```gitignore
|
|
105
106
|
/repos-local.txt
|
|
@@ -214,7 +215,11 @@ For reusable Rust libraries, keep the core crate Python-free and put PyO3 in an
|
|
|
214
215
|
|
|
215
216
|
Set `native-binaries = true` under `[tool.fastws]` to build and install the binary targets of the workspace's default members into the active venv alongside the extension. A binary can live in its own member crate, such as a `cli/` crate beside `py/`, listed in `default-members`. maturin's sdist keeps only the members that the extension depends on, so a wheel built from the sdist lacks a binary from such a crate. Cargo reports the executable paths; there is no separate list of binary names.
|
|
216
217
|
|
|
217
|
-
|
|
218
|
+
`cargo develop` also copies the `data/` subdirectory of `[tool.maturin].data` into the active venv, as a wheel install does. For example, `data/share/jupyter/kernels/<name>/kernel.json` is installed as `share/jupyter/kernels/<name>/kernel.json`, where Jupyter finds the kernel. `cargo develop` installs the binaries it builds, not the staged copies in `scripts/`.
|
|
219
|
+
|
|
220
|
+
For distribution wheels containing these binaries, use `fastws.build_backend` as the build backend and include `fastws-cli>=0.0.20` alongside maturin in `[build-system].requires`. Set `[tool.maturin].data` to a generated wheel-data directory. The backend replaces its `scripts/` subdirectory with the current Python-free binaries, then delegates packaging to maturin. Explicit profile and target arguments from maturin's build settings also apply to the binaries.
|
|
221
|
+
|
|
222
|
+
Editable builds through this backend install only Python metadata and the source path. `ws-sync` then runs `cargo develop` to build and install the extension, the binaries and the wheel data files. After standalone `uv sync`, run `cargo develop` yourself.
|
|
218
223
|
|
|
219
224
|
Direct maturin CLI builds bypass the Python build backend. Stage the binaries first with the shared command:
|
|
220
225
|
|
|
@@ -240,7 +245,7 @@ Fastws maintains `tool.uv.workspace.exclude`. It excludes unlisted top-level dir
|
|
|
240
245
|
|
|
241
246
|
Existing globs, entries for missing directories, and entries for checkouts that are still not Python projects are retained. Use `exclude = [...]` under `[tool.fastws]` to specify exclusions the scan cannot infer, such as keeping a valid project out of the workspace. Adding members preserves hand-written `[tool.uv.sources]` entries, including path and Git sources.
|
|
242
247
|
|
|
243
|
-
Each sync regenerates Cargo overrides in the workspace's `.cargo/config.toml`. `[patch.crates-io]` gets an entry for each local crate, including nested Cargo workspace members. Git dependencies that name local crates get entries under the matching URL. Builds under the workspace root use these local checkouts. Entries pointing outside the root and other configuration sections are preserved.
|
|
248
|
+
Each sync regenerates Cargo overrides in the workspace's `.cargo/config.toml`. `[patch.crates-io]` gets an entry for each local crate, including nested Cargo workspace members, except a crate marked `publish = false`, which never comes from crates.io. Git dependencies that name local crates get entries under the matching URL. Builds under the workspace root use these local checkouts. Entries pointing outside the root and other configuration sections are preserved.
|
|
244
249
|
|
|
245
250
|
Do not commit a `Cargo.lock` generated under these patches. Its source-less local entries cannot be resolved on another machine.
|
|
246
251
|
|
|
@@ -252,7 +257,9 @@ Each sync updates `workspaces` in the root `package.json`. It creates the file w
|
|
|
252
257
|
|
|
253
258
|
An optional tracked `package.json.shared` is merged into the root `package.json` on every sync. Object-valued fields merge one level deep, shared values win conflicts, and arrays replace rather than concatenate. Unrelated local settings remain; removing a shared key does not delete its local value. Workspace discovery still manages `workspaces`. Use this file for shared policy such as version-pinned npm `allowScripts` approvals.
|
|
254
259
|
|
|
255
|
-
|
|
260
|
+
Before `uv sync`, fastws regenerates `tool.uv.no-build-isolation-package` with the sorted package names of projects using `fastws.build_backend`, including external checkouts. These builds use the shared environment's `fastws-cli` and `maturin`. Leave global build isolation enabled for other projects.
|
|
261
|
+
|
|
262
|
+
After `uv sync`, fastws runs `cargo develop` in each mixed Rust/Python project. Cargo skips unchanged compilation inputs. Projects using `fastws.build_backend` should keep only packaging inputs in `tool.uv.cache-keys`; Rust source changes are handled by Cargo. Maturin packages with `bindings = "bin"` keep their existing uv installation path.
|
|
256
263
|
|
|
257
264
|
Fastws then runs `<tool> install` at the workspace root. It runs `<tool> run build` in each JavaScript package containing both `Cargo.toml` and a `build` script. Cargo handles incremental compilation. Other build steps still run.
|
|
258
265
|
|
|
@@ -74,7 +74,7 @@ Keep `repos.txt` tracked in the workspace Git repo. Put personal additions in a
|
|
|
74
74
|
|
|
75
75
|
`--repos-file myrepos.txt` uses `myrepos-local.txt` alongside it. Either list may be absent. Removing a repo from the baseline does not delete its checkout. The next sync discovers an existing root checkout as a personal addition. External checkouts also remain on disk; add them to the local list to keep managing them.
|
|
76
76
|
|
|
77
|
-
The workspace `pyproject.toml` is local generated state. `ws-sync` creates it from the tracked `pyproject.tmpl` when available, or generates a minimal one, then maintains it while preserving personal settings except for the repo extras
|
|
77
|
+
The workspace `pyproject.toml` is local generated state. `ws-sync` creates it from the tracked `pyproject.tmpl` when available, or generates a minimal one, then maintains it while preserving personal settings except for the managed repo extras, workspace exclusions, and build-isolation list. The template supplies starting defaults only: later template edits do not overwrite existing configuration. Ignore these files in the workspace repo (not in its member repos):
|
|
78
78
|
|
|
79
79
|
```gitignore
|
|
80
80
|
/repos-local.txt
|
|
@@ -189,7 +189,11 @@ For reusable Rust libraries, keep the core crate Python-free and put PyO3 in an
|
|
|
189
189
|
|
|
190
190
|
Set `native-binaries = true` under `[tool.fastws]` to build and install the binary targets of the workspace's default members into the active venv alongside the extension. A binary can live in its own member crate, such as a `cli/` crate beside `py/`, listed in `default-members`. maturin's sdist keeps only the members that the extension depends on, so a wheel built from the sdist lacks a binary from such a crate. Cargo reports the executable paths; there is no separate list of binary names.
|
|
191
191
|
|
|
192
|
-
|
|
192
|
+
`cargo develop` also copies the `data/` subdirectory of `[tool.maturin].data` into the active venv, as a wheel install does. For example, `data/share/jupyter/kernels/<name>/kernel.json` is installed as `share/jupyter/kernels/<name>/kernel.json`, where Jupyter finds the kernel. `cargo develop` installs the binaries it builds, not the staged copies in `scripts/`.
|
|
193
|
+
|
|
194
|
+
For distribution wheels containing these binaries, use `fastws.build_backend` as the build backend and include `fastws-cli>=0.0.20` alongside maturin in `[build-system].requires`. Set `[tool.maturin].data` to a generated wheel-data directory. The backend replaces its `scripts/` subdirectory with the current Python-free binaries, then delegates packaging to maturin. Explicit profile and target arguments from maturin's build settings also apply to the binaries.
|
|
195
|
+
|
|
196
|
+
Editable builds through this backend install only Python metadata and the source path. `ws-sync` then runs `cargo develop` to build and install the extension, the binaries and the wheel data files. After standalone `uv sync`, run `cargo develop` yourself.
|
|
193
197
|
|
|
194
198
|
Direct maturin CLI builds bypass the Python build backend. Stage the binaries first with the shared command:
|
|
195
199
|
|
|
@@ -215,7 +219,7 @@ Fastws maintains `tool.uv.workspace.exclude`. It excludes unlisted top-level dir
|
|
|
215
219
|
|
|
216
220
|
Existing globs, entries for missing directories, and entries for checkouts that are still not Python projects are retained. Use `exclude = [...]` under `[tool.fastws]` to specify exclusions the scan cannot infer, such as keeping a valid project out of the workspace. Adding members preserves hand-written `[tool.uv.sources]` entries, including path and Git sources.
|
|
217
221
|
|
|
218
|
-
Each sync regenerates Cargo overrides in the workspace's `.cargo/config.toml`. `[patch.crates-io]` gets an entry for each local crate, including nested Cargo workspace members. Git dependencies that name local crates get entries under the matching URL. Builds under the workspace root use these local checkouts. Entries pointing outside the root and other configuration sections are preserved.
|
|
222
|
+
Each sync regenerates Cargo overrides in the workspace's `.cargo/config.toml`. `[patch.crates-io]` gets an entry for each local crate, including nested Cargo workspace members, except a crate marked `publish = false`, which never comes from crates.io. Git dependencies that name local crates get entries under the matching URL. Builds under the workspace root use these local checkouts. Entries pointing outside the root and other configuration sections are preserved.
|
|
219
223
|
|
|
220
224
|
Do not commit a `Cargo.lock` generated under these patches. Its source-less local entries cannot be resolved on another machine.
|
|
221
225
|
|
|
@@ -227,7 +231,9 @@ Each sync updates `workspaces` in the root `package.json`. It creates the file w
|
|
|
227
231
|
|
|
228
232
|
An optional tracked `package.json.shared` is merged into the root `package.json` on every sync. Object-valued fields merge one level deep, shared values win conflicts, and arrays replace rather than concatenate. Unrelated local settings remain; removing a shared key does not delete its local value. Workspace discovery still manages `workspaces`. Use this file for shared policy such as version-pinned npm `allowScripts` approvals.
|
|
229
233
|
|
|
230
|
-
|
|
234
|
+
Before `uv sync`, fastws regenerates `tool.uv.no-build-isolation-package` with the sorted package names of projects using `fastws.build_backend`, including external checkouts. These builds use the shared environment's `fastws-cli` and `maturin`. Leave global build isolation enabled for other projects.
|
|
235
|
+
|
|
236
|
+
After `uv sync`, fastws runs `cargo develop` in each mixed Rust/Python project. Cargo skips unchanged compilation inputs. Projects using `fastws.build_backend` should keep only packaging inputs in `tool.uv.cache-keys`; Rust source changes are handled by Cargo. Maturin packages with `bindings = "bin"` keep their existing uv installation path.
|
|
231
237
|
|
|
232
238
|
Fastws then runs `<tool> install` at the workspace root. It runs `<tool> run build` in each JavaScript package containing both `Cargo.toml` and a `build` script. Cargo handles incremental compilation. Other build steps still run.
|
|
233
239
|
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
r"Maturin build backend that includes Cargo binaries in Python wheels."
|
|
2
|
-
import argparse
|
|
2
|
+
import argparse, base64, csv, hashlib, io
|
|
3
3
|
from pathlib import Path
|
|
4
|
+
from tempfile import TemporaryDirectory
|
|
5
|
+
from zipfile import ZipFile, ZIP_DEFLATED
|
|
4
6
|
import maturin
|
|
5
7
|
from .cargo import settings, stage_binaries
|
|
6
8
|
|
|
@@ -22,8 +24,25 @@ def build_wheel(wheel_directory, config_settings=None, metadata_directory=None):
|
|
|
22
24
|
|
|
23
25
|
|
|
24
26
|
def build_editable(wheel_directory, config_settings=None, metadata_directory=None):
|
|
25
|
-
|
|
26
|
-
|
|
27
|
+
r"Build an editable wheel containing metadata and the Python source path, without compiling Rust."
|
|
28
|
+
with TemporaryDirectory() as tmp:
|
|
29
|
+
info = Path(metadata_directory) if metadata_directory else Path(tmp)/prepare_metadata_for_build_editable(tmp, config_settings)
|
|
30
|
+
name = info.name.removesuffix('.dist-info')
|
|
31
|
+
tag = next(line.removeprefix('Tag: ') for line in (info/'WHEEL').read_text().splitlines() if line.startswith('Tag: '))
|
|
32
|
+
files = {p.relative_to(info.parent).as_posix(): p.read_bytes() for p in info.rglob('*') if p.is_file() and p != info/'RECORD'}
|
|
33
|
+
root = Path.cwd()
|
|
34
|
+
source = (root/settings(root)['maturin']['python-source']).resolve()
|
|
35
|
+
files[f'{name}.pth'] = f'{source}\n'.encode()
|
|
36
|
+
record = f'{info.name}/RECORD'
|
|
37
|
+
rows = [(path, 'sha256=' + base64.urlsafe_b64encode(hashlib.sha256(data).digest()).rstrip(b'=').decode(), len(data))
|
|
38
|
+
for path, data in files.items()]
|
|
39
|
+
buffer = io.StringIO()
|
|
40
|
+
csv.writer(buffer).writerows([*rows, (record, '', '')])
|
|
41
|
+
files[record] = buffer.getvalue().encode()
|
|
42
|
+
filename = f'{name}-{tag}.whl'
|
|
43
|
+
with ZipFile(Path(wheel_directory)/filename, 'w', ZIP_DEFLATED) as wheel:
|
|
44
|
+
for path, data in files.items(): wheel.writestr(path, data)
|
|
45
|
+
return filename
|
|
27
46
|
|
|
28
47
|
|
|
29
48
|
def prepare_metadata_for_build_wheel(metadata_directory, config_settings=None):
|
|
@@ -49,7 +49,7 @@ def replace(source, destination):
|
|
|
49
49
|
|
|
50
50
|
|
|
51
51
|
def develop(root):
|
|
52
|
-
"Install a Python extension
|
|
52
|
+
"Install a Python extension, opted-in binaries and wheel data files from one Cargo build."
|
|
53
53
|
tool = settings(root)
|
|
54
54
|
cfg = tool['maturin']
|
|
55
55
|
if 'python-source' not in cfg: raise SystemExit(r'cargo develop requires tool.maturin.python-source')
|
|
@@ -59,12 +59,17 @@ def develop(root):
|
|
|
59
59
|
lib, = [f for f in artifact['filenames'] if Path(f).suffix in ('.dylib', '.so', '.dll')]
|
|
60
60
|
module = cfg.get('module-name', artifact['target']['name'])
|
|
61
61
|
dest = root/cfg['python-source']/Path(*module.split('.'))
|
|
62
|
-
|
|
62
|
+
replace(lib, dest.with_name(dest.name + sysconfig.get_config_var('EXT_SUFFIX')))
|
|
63
|
+
env = Path(os.environ.get('VIRTUAL_ENV', sys.prefix))
|
|
63
64
|
if binaries:
|
|
64
|
-
env = Path(os.environ.get('VIRTUAL_ENV', sys.prefix))
|
|
65
65
|
scripts = env/('Scripts' if os.name == 'nt' else 'bin')
|
|
66
|
-
|
|
67
|
-
|
|
66
|
+
for m in artifacts:
|
|
67
|
+
if m.get('executable'): replace(m['executable'], scripts/Path(m['executable']).name)
|
|
68
|
+
# Editable builds through `build_backend` don't install the wheel's data files, which a wheel install puts under the prefix.
|
|
69
|
+
if 'data' in cfg:
|
|
70
|
+
data = root/cfg['data']/'data'
|
|
71
|
+
for f in data.rglob('*'):
|
|
72
|
+
if f.is_file(): replace(f, env/f.relative_to(data))
|
|
68
73
|
|
|
69
74
|
|
|
70
75
|
def stage_binaries(root, profile=None, target=None):
|
|
@@ -172,7 +172,7 @@ def _remove_from_pyproject(pyproject_path: Path, names: list[str]) -> list[str]:
|
|
|
172
172
|
sources = {k:v for k,v in sources.items() if _pkg_key(k) not in targets}
|
|
173
173
|
deps = [d for d in deps if _pkg_key(dep_key(d)) not in targets]
|
|
174
174
|
content = _replace_table(content, "tool.uv.sources", "\n".join(f"{k} = {_fmt_source(v)}" for k,v in sources.items()))
|
|
175
|
-
content =
|
|
175
|
+
content = _replace_array(content, "project", "dependencies", deps)
|
|
176
176
|
pyproject_path.write_text(content)
|
|
177
177
|
return removed
|
|
178
178
|
|
|
@@ -247,17 +247,17 @@ def _sync_ws_excludes(pyproject_path: Path, root: Path, tracked: set[str]) -> tu
|
|
|
247
247
|
survivors = set(kept) | set(auto)
|
|
248
248
|
new = [e for e in cur if e in survivors] + [e for e in kept + auto if e not in cur]
|
|
249
249
|
if new == cur: return [], []
|
|
250
|
-
content =
|
|
250
|
+
content = _replace_array(content, "tool.uv.workspace", "exclude", new)
|
|
251
251
|
tomllib.loads(content) # never write an unparseable pyproject
|
|
252
252
|
pyproject_path.write_text(content)
|
|
253
253
|
return [e for e in new if e not in cur], [e for e in cur if e not in new]
|
|
254
254
|
|
|
255
|
-
def
|
|
256
|
-
block = "
|
|
257
|
-
if not (span := _table_span(content,
|
|
255
|
+
def _replace_array(content: str, table: str, key: str, values: list[str]) -> str:
|
|
256
|
+
block = f"{key} = [\n" + "".join(f' {json.dumps(v)},\n' for v in values) + "]"
|
|
257
|
+
if not (span := _table_span(content, table)): return _replace_table(content, table, block)
|
|
258
258
|
start,end = span
|
|
259
259
|
section = content[start:end]
|
|
260
|
-
if m := re.search(
|
|
260
|
+
if m := re.search(rf"(?m)^{re.escape(key)}\s*=\s*\[", section):
|
|
261
261
|
arr_start = m.end()-1
|
|
262
262
|
arr_end = _find_array_end(section, arr_start)
|
|
263
263
|
section = section[:m.start()] + block + section[arr_end+1:]
|
|
@@ -293,18 +293,6 @@ def _find_array_end(content: str, start: int) -> int:
|
|
|
293
293
|
if depth == 0: return i
|
|
294
294
|
raise ValueError("Unterminated TOML array")
|
|
295
295
|
|
|
296
|
-
def _replace_project_dependencies(content: str, deps: list[str]) -> str:
|
|
297
|
-
if not (span := _table_span(content, "project")): raise SystemExit("Missing [project] table in pyproject.toml")
|
|
298
|
-
start,end = span
|
|
299
|
-
section = content[start:end]
|
|
300
|
-
dep_block = "dependencies = [\n" + "".join(f' {json.dumps(dep)},\n' for dep in deps) + "]"
|
|
301
|
-
if m := re.search(r"(?m)^dependencies\s*=\s*\[", section):
|
|
302
|
-
arr_start = m.end()-1
|
|
303
|
-
arr_end = _find_array_end(section, arr_start)
|
|
304
|
-
section = section[:m.start()] + dep_block + section[arr_end+1:]
|
|
305
|
-
else: section = section.rstrip() + "\n" + dep_block + "\n"
|
|
306
|
-
return content[:start] + section + content[end:]
|
|
307
|
-
|
|
308
296
|
def _init_ws_pyproject(path: Path, python: str = f"{sys.version_info.major}.{sys.version_info.minor}") -> bool:
|
|
309
297
|
if path.exists(): return False
|
|
310
298
|
major,minor = map(int, python.split(".")[:2])
|
|
@@ -348,10 +336,21 @@ def _sync_ws_pyproject(pyproject_path: Path, template_path: Path, projects: list
|
|
|
348
336
|
if missing or ext_missing:
|
|
349
337
|
source_lines = "\n".join(f"{proj} = {_fmt_source(src)}" for proj,src in sources.items())
|
|
350
338
|
content = _replace_table(content, "tool.uv.sources", source_lines)
|
|
351
|
-
if deps != data.get("project", {}).get("dependencies", []): content =
|
|
339
|
+
if deps != data.get("project", {}).get("dependencies", []): content = _replace_array(content, "project", "dependencies", deps)
|
|
352
340
|
if content != original: pyproject_path.write_text(content)
|
|
353
341
|
return missing + [n for n,_ in ext_missing]
|
|
354
342
|
|
|
343
|
+
def _sync_build_isolation(pyproject_path: Path, projects: list[tuple[str, Path]]):
|
|
344
|
+
"Regenerate uv's non-isolated build list from the projects using the fastws backend."
|
|
345
|
+
content = pyproject_path.read_text()
|
|
346
|
+
packages = sorted({name for name,d in projects
|
|
347
|
+
if tomllib.loads((d/'pyproject.toml').read_text()).get('build-system', {}).get('build-backend') == 'fastws.build_backend'})
|
|
348
|
+
current = tomllib.loads(content).get('tool', {}).get('uv', {}).get('no-build-isolation-package', [])
|
|
349
|
+
if packages == current: return
|
|
350
|
+
content = _replace_array(content, 'tool.uv', 'no-build-isolation-package', packages)
|
|
351
|
+
tomllib.loads(content)
|
|
352
|
+
pyproject_path.write_text(content)
|
|
353
|
+
|
|
355
354
|
def _editable_mapping(path: Path) -> dict[str,str]:
|
|
356
355
|
tree = ast.parse(path.read_text(), filename=str(path))
|
|
357
356
|
for node in tree.body:
|
|
@@ -564,7 +563,7 @@ def cargo_develop(
|
|
|
564
563
|
):
|
|
565
564
|
r"Build and install a mixed Rust/Python project's extension and opted-in binaries."
|
|
566
565
|
from .cargo import develop
|
|
567
|
-
|
|
566
|
+
develop(Path(path).resolve())
|
|
568
567
|
|
|
569
568
|
@call_parse(pos=['command'])
|
|
570
569
|
def cargo_stage(
|
|
@@ -645,18 +644,18 @@ def _cargo_patches(root: Path):
|
|
|
645
644
|
return patches, config
|
|
646
645
|
|
|
647
646
|
def _crate_pkgs(d: Path):
|
|
648
|
-
"(package
|
|
647
|
+
"(`[package]` table, dir) for the crate at `d` and its cargo workspace members"
|
|
649
648
|
try: data = tomllib.loads((d/"Cargo.toml").read_text())
|
|
650
649
|
except tomllib.TOMLDecodeError: return
|
|
651
|
-
if name := data.get("package", {})
|
|
650
|
+
if "name" in (pkg := data.get("package", {})): yield pkg, d
|
|
652
651
|
for m in _project_dirs(d, "Cargo.toml", data.get("workspace", {}).get("members", [])):
|
|
653
652
|
try: sub = tomllib.loads((m/"Cargo.toml").read_text())
|
|
654
653
|
except tomllib.TOMLDecodeError: continue
|
|
655
|
-
if name := sub.get("package", {})
|
|
654
|
+
if "name" in (pkg := sub.get("package", {})): yield pkg, m
|
|
656
655
|
|
|
657
|
-
def _local_crates(root: Path) -> dict
|
|
658
|
-
"
|
|
659
|
-
return
|
|
656
|
+
def _local_crates(root: Path) -> list[tuple[dict, Path]]:
|
|
657
|
+
"(`[package]` table, dir) for every crate under `root`, nested cargo workspace members included"
|
|
658
|
+
return [p for crate in _crate_dirs(root) for p in _crate_pkgs(crate)]
|
|
660
659
|
|
|
661
660
|
def _git_dep_tables(root: Path, crates: dict[str, Path]) -> dict[str, dict[str, Path]]:
|
|
662
661
|
"Git URL -> {package: local dir} for members' git deps that name a local crate"
|
|
@@ -688,14 +687,15 @@ def _entry_changes(current, new):
|
|
|
688
687
|
def _sync_cargo_patches(root: Path) -> tuple[list[str], list[str]]:
|
|
689
688
|
"""Regenerate `[patch]` entries in the workspace `.cargo/config.toml` and return (added, removed).
|
|
690
689
|
|
|
691
|
-
Every local crate gets a `[patch.crates-io]` entry,
|
|
692
|
-
an entry under that URL
|
|
693
|
-
editable installs. Only entries whose path is inside `root` are
|
|
694
|
-
elsewhere (and any other config sections) are kept as-is, and a kept entry wins
|
|
695
|
-
one of the same name."""
|
|
690
|
+
Every local crate gets a `[patch.crates-io]` entry, except one marked `publish = false`, which never comes
|
|
691
|
+
from crates.io. A member's git dep on a local crate gets an entry under that URL. Builds anywhere under `root`
|
|
692
|
+
then use the checkouts: the cargo analog of editable installs. Only entries whose path is inside `root` are
|
|
693
|
+
managed; entries pointing elsewhere (and any other config sections) are kept as-is, and a kept entry wins
|
|
694
|
+
over a generated one of the same name."""
|
|
696
695
|
root = root.resolve()
|
|
697
|
-
|
|
698
|
-
|
|
696
|
+
pkgs = _local_crates(root)
|
|
697
|
+
crates = {pkg["name"]: d for pkg, d in pkgs}
|
|
698
|
+
desired = {"crates-io": {pkg["name"]: str(d) for pkg, d in pkgs if pkg.get("publish") is not False}}
|
|
699
699
|
for url, entries in _git_dep_tables(root, crates).items(): desired[url] = {name: str(d) for name, d in entries.items()}
|
|
700
700
|
config = root/".cargo"/"config.toml"
|
|
701
701
|
content = config.read_text() if config.exists() else ""
|
|
@@ -923,6 +923,8 @@ async def ws_sync(
|
|
|
923
923
|
missing_projects = _sync_ws_pyproject(pyproject_path, template_path, _ws_projects(root), _external_projects(root, ext_dirs),
|
|
924
924
|
_repo_extras(root, entries))
|
|
925
925
|
if missing_projects: print(f"Added workspace projects: {', '.join(missing_projects)}")
|
|
926
|
+
projects = _build_projects(root, repos_file)
|
|
927
|
+
_sync_build_isolation(pyproject_path, projects)
|
|
926
928
|
|
|
927
929
|
wrapper_added = _sync_cargo_wrapper(root)
|
|
928
930
|
added_p, removed_p = _sync_cargo_patches(root)
|
|
@@ -943,7 +945,7 @@ async def ws_sync(
|
|
|
943
945
|
if up: _cargo_update(root, workers=workers)
|
|
944
946
|
_sync_cargo_keys(root)
|
|
945
947
|
subprocess.run(["uv", "sync", "-U"] if up else ["uv", "sync"], check=True, cwd=root)
|
|
946
|
-
for _,d in
|
|
948
|
+
for _,d in projects:
|
|
947
949
|
cfg = tomllib.loads((d/'pyproject.toml').read_text()).get('tool', {}).get('maturin', {})
|
|
948
950
|
if 'python-source' in cfg and cfg.get('bindings') != 'bin': cargo_develop(path=str(d))
|
|
949
951
|
if up: _upgrade_stamp(root).touch()
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: fastws-cli
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.24
|
|
4
4
|
Summary: Fast workspace tools for multi-repo management
|
|
5
5
|
Author: Jeremy Howard
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -14,6 +14,7 @@ Requires-Dist: fastcore>=2.2.33
|
|
|
14
14
|
Requires-Dist: fastgit>=0.1.2
|
|
15
15
|
Requires-Dist: ghapi>=2.0.6
|
|
16
16
|
Requires-Dist: packaging
|
|
17
|
+
Requires-Dist: maturin<2.0,>=1.9.4
|
|
17
18
|
Requires-Dist: tomli; python_version < "3.11"
|
|
18
19
|
Provides-Extra: dev
|
|
19
20
|
Requires-Dist: fastship; extra == "dev"
|
|
@@ -99,7 +100,7 @@ Keep `repos.txt` tracked in the workspace Git repo. Put personal additions in a
|
|
|
99
100
|
|
|
100
101
|
`--repos-file myrepos.txt` uses `myrepos-local.txt` alongside it. Either list may be absent. Removing a repo from the baseline does not delete its checkout. The next sync discovers an existing root checkout as a personal addition. External checkouts also remain on disk; add them to the local list to keep managing them.
|
|
101
102
|
|
|
102
|
-
The workspace `pyproject.toml` is local generated state. `ws-sync` creates it from the tracked `pyproject.tmpl` when available, or generates a minimal one, then maintains it while preserving personal settings except for the repo extras
|
|
103
|
+
The workspace `pyproject.toml` is local generated state. `ws-sync` creates it from the tracked `pyproject.tmpl` when available, or generates a minimal one, then maintains it while preserving personal settings except for the managed repo extras, workspace exclusions, and build-isolation list. The template supplies starting defaults only: later template edits do not overwrite existing configuration. Ignore these files in the workspace repo (not in its member repos):
|
|
103
104
|
|
|
104
105
|
```gitignore
|
|
105
106
|
/repos-local.txt
|
|
@@ -214,7 +215,11 @@ For reusable Rust libraries, keep the core crate Python-free and put PyO3 in an
|
|
|
214
215
|
|
|
215
216
|
Set `native-binaries = true` under `[tool.fastws]` to build and install the binary targets of the workspace's default members into the active venv alongside the extension. A binary can live in its own member crate, such as a `cli/` crate beside `py/`, listed in `default-members`. maturin's sdist keeps only the members that the extension depends on, so a wheel built from the sdist lacks a binary from such a crate. Cargo reports the executable paths; there is no separate list of binary names.
|
|
216
217
|
|
|
217
|
-
|
|
218
|
+
`cargo develop` also copies the `data/` subdirectory of `[tool.maturin].data` into the active venv, as a wheel install does. For example, `data/share/jupyter/kernels/<name>/kernel.json` is installed as `share/jupyter/kernels/<name>/kernel.json`, where Jupyter finds the kernel. `cargo develop` installs the binaries it builds, not the staged copies in `scripts/`.
|
|
219
|
+
|
|
220
|
+
For distribution wheels containing these binaries, use `fastws.build_backend` as the build backend and include `fastws-cli>=0.0.20` alongside maturin in `[build-system].requires`. Set `[tool.maturin].data` to a generated wheel-data directory. The backend replaces its `scripts/` subdirectory with the current Python-free binaries, then delegates packaging to maturin. Explicit profile and target arguments from maturin's build settings also apply to the binaries.
|
|
221
|
+
|
|
222
|
+
Editable builds through this backend install only Python metadata and the source path. `ws-sync` then runs `cargo develop` to build and install the extension, the binaries and the wheel data files. After standalone `uv sync`, run `cargo develop` yourself.
|
|
218
223
|
|
|
219
224
|
Direct maturin CLI builds bypass the Python build backend. Stage the binaries first with the shared command:
|
|
220
225
|
|
|
@@ -240,7 +245,7 @@ Fastws maintains `tool.uv.workspace.exclude`. It excludes unlisted top-level dir
|
|
|
240
245
|
|
|
241
246
|
Existing globs, entries for missing directories, and entries for checkouts that are still not Python projects are retained. Use `exclude = [...]` under `[tool.fastws]` to specify exclusions the scan cannot infer, such as keeping a valid project out of the workspace. Adding members preserves hand-written `[tool.uv.sources]` entries, including path and Git sources.
|
|
242
247
|
|
|
243
|
-
Each sync regenerates Cargo overrides in the workspace's `.cargo/config.toml`. `[patch.crates-io]` gets an entry for each local crate, including nested Cargo workspace members. Git dependencies that name local crates get entries under the matching URL. Builds under the workspace root use these local checkouts. Entries pointing outside the root and other configuration sections are preserved.
|
|
248
|
+
Each sync regenerates Cargo overrides in the workspace's `.cargo/config.toml`. `[patch.crates-io]` gets an entry for each local crate, including nested Cargo workspace members, except a crate marked `publish = false`, which never comes from crates.io. Git dependencies that name local crates get entries under the matching URL. Builds under the workspace root use these local checkouts. Entries pointing outside the root and other configuration sections are preserved.
|
|
244
249
|
|
|
245
250
|
Do not commit a `Cargo.lock` generated under these patches. Its source-less local entries cannot be resolved on another machine.
|
|
246
251
|
|
|
@@ -252,7 +257,9 @@ Each sync updates `workspaces` in the root `package.json`. It creates the file w
|
|
|
252
257
|
|
|
253
258
|
An optional tracked `package.json.shared` is merged into the root `package.json` on every sync. Object-valued fields merge one level deep, shared values win conflicts, and arrays replace rather than concatenate. Unrelated local settings remain; removing a shared key does not delete its local value. Workspace discovery still manages `workspaces`. Use this file for shared policy such as version-pinned npm `allowScripts` approvals.
|
|
254
259
|
|
|
255
|
-
|
|
260
|
+
Before `uv sync`, fastws regenerates `tool.uv.no-build-isolation-package` with the sorted package names of projects using `fastws.build_backend`, including external checkouts. These builds use the shared environment's `fastws-cli` and `maturin`. Leave global build isolation enabled for other projects.
|
|
261
|
+
|
|
262
|
+
After `uv sync`, fastws runs `cargo develop` in each mixed Rust/Python project. Cargo skips unchanged compilation inputs. Projects using `fastws.build_backend` should keep only packaging inputs in `tool.uv.cache-keys`; Rust source changes are handled by Cargo. Maturin packages with `bindings = "bin"` keep their existing uv installation path.
|
|
256
263
|
|
|
257
264
|
Fastws then runs `<tool> install` at the workspace root. It runs `<tool> run build` in each JavaScript package containing both `Cargo.toml` and a `build` script. Cargo handles incremental compilation. Other build steps still run.
|
|
258
265
|
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import base64, csv, hashlib, io
|
|
2
|
+
from zipfile import ZipFile
|
|
3
|
+
from fastws.build_backend import build_editable
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def test_editable_wheel(tmp_path, monkeypatch):
|
|
7
|
+
monkeypatch.chdir(tmp_path)
|
|
8
|
+
(tmp_path/'pyproject.toml').write_text(r'''[tool.maturin]
|
|
9
|
+
python-source = "python"
|
|
10
|
+
data = "wheel"
|
|
11
|
+
''')
|
|
12
|
+
info = tmp_path/'demo-1.0.dist-info'
|
|
13
|
+
info.mkdir()
|
|
14
|
+
(info/'WHEEL').write_text('Wheel-Version: 1.0\nRoot-Is-Purelib: false\nTag: py3-none-any\n')
|
|
15
|
+
(info/'METADATA').write_text('Metadata-Version: 2.1\nName: demo\nVersion: 1.0\n')
|
|
16
|
+
(info/'entry_points.txt').write_text('[console_scripts]\ndemo = demo:main\n')
|
|
17
|
+
with ZipFile(tmp_path/build_editable(tmp_path, metadata_directory=info)) as wheel:
|
|
18
|
+
assert wheel.read('demo-1.0.pth').decode() == str(tmp_path/'python') + '\n'
|
|
19
|
+
assert wheel.read('demo-1.0.dist-info/entry_points.txt') == (info/'entry_points.txt').read_bytes()
|
|
20
|
+
record = list(csv.reader(io.StringIO(wheel.read('demo-1.0.dist-info/RECORD').decode())))
|
|
21
|
+
assert {row[0] for row in record} == set(wheel.namelist())
|
|
22
|
+
for path, digest, size in record[:-1]:
|
|
23
|
+
data = wheel.read(path)
|
|
24
|
+
assert digest == 'sha256=' + base64.urlsafe_b64encode(hashlib.sha256(data).digest()).rstrip(b'=').decode()
|
|
25
|
+
assert int(size) == len(data)
|
|
26
|
+
assert record[-1] == ['demo-1.0.dist-info/RECORD', '', '']
|
|
@@ -155,17 +155,17 @@ def test_sync_cargo_patches_generates_and_preserves(tmp_path):
|
|
|
155
155
|
(crate1/'Cargo.toml').write_text('[package]\nname = "crate1"\nversion = "0.1.0"\n\n[dependencies]\nfamily = { git = "https://example.com/family" }\n')
|
|
156
156
|
family = tmp_path/'family'
|
|
157
157
|
(family/'sub').mkdir(parents=True)
|
|
158
|
-
(family/'Cargo.toml').write_text('[package]\nname = "family"\nversion = "0.1.0"\n\n[workspace]\nmembers = ["sub"]\n')
|
|
158
|
+
(family/'Cargo.toml').write_text('[package]\nname = "family"\nversion = "0.1.0"\npublish = false\n\n[workspace]\nmembers = ["sub"]\n')
|
|
159
159
|
(family/'sub'/'Cargo.toml').write_text('[package]\nname = "family-sub"\nversion = "0.1.0"\n')
|
|
160
160
|
|
|
161
161
|
added, removed = core._sync_cargo_patches(tmp_path)
|
|
162
162
|
data = core.tomllib.loads(config.read_text())
|
|
163
163
|
cio = data['patch']['crates-io']
|
|
164
|
-
assert set(cio) == {'foreign', 'crate1', 'family
|
|
164
|
+
assert set(cio) == {'foreign', 'crate1', 'family-sub'} # `publish = false` crates never come from crates.io
|
|
165
165
|
assert cio['foreign']['path'] == '/elsewhere/foreign' # entries pointing outside the root are kept as-is
|
|
166
166
|
assert cio['crate1']['path'] == str(crate1)
|
|
167
167
|
assert cio['family-sub']['path'] == str(family/'sub')
|
|
168
|
-
assert data['patch']['https://example.com/family']['family']['path'] == str(family) # git deps on local crates get their URL table
|
|
168
|
+
assert data['patch']['https://example.com/family']['family']['path'] == str(family) # git deps on local crates get their URL table, published or not
|
|
169
169
|
assert data['term']['quiet'] is True # other sections untouched
|
|
170
170
|
assert set(added) == {'crate1', 'family', 'family-sub'} and removed == ['gone']
|
|
171
171
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|