fastws-cli 0.0.32__tar.gz → 0.0.34__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.
Files changed (33) hide show
  1. {fastws_cli-0.0.32 → fastws_cli-0.0.34}/PKG-INFO +60 -6
  2. {fastws_cli-0.0.32 → fastws_cli-0.0.34}/README.md +59 -5
  3. {fastws_cli-0.0.32 → fastws_cli-0.0.34}/fastws/__init__.py +3 -1
  4. fastws_cli-0.0.34/fastws/_toml.py +4 -0
  5. fastws_cli-0.0.34/fastws/cargo.py +292 -0
  6. {fastws_cli-0.0.32 → fastws_cli-0.0.34}/fastws/core.py +57 -26
  7. fastws_cli-0.0.34/fastws/integrate.py +136 -0
  8. fastws_cli-0.0.34/fastws/procs.py +46 -0
  9. fastws_cli-0.0.34/fastws/store.py +170 -0
  10. fastws_cli-0.0.34/fastws/timelock.py +76 -0
  11. {fastws_cli-0.0.32 → fastws_cli-0.0.34}/fastws_cli.egg-info/PKG-INFO +60 -6
  12. {fastws_cli-0.0.32 → fastws_cli-0.0.34}/fastws_cli.egg-info/SOURCES.txt +10 -1
  13. {fastws_cli-0.0.32 → fastws_cli-0.0.34}/pyproject.toml +3 -0
  14. {fastws_cli-0.0.32 → fastws_cli-0.0.34}/tests/test_build.py +33 -10
  15. fastws_cli-0.0.34/tests/test_integrate.py +46 -0
  16. fastws_cli-0.0.34/tests/test_procs.py +35 -0
  17. fastws_cli-0.0.34/tests/test_store.py +33 -0
  18. {fastws_cli-0.0.32 → fastws_cli-0.0.34}/tests/test_sync.py +10 -0
  19. fastws_cli-0.0.34/tests/test_timelock.py +21 -0
  20. fastws_cli-0.0.32/fastws/cargo.py +0 -295
  21. {fastws_cli-0.0.32 → fastws_cli-0.0.34}/LICENSE +0 -0
  22. {fastws_cli-0.0.32 → fastws_cli-0.0.34}/fastws/build_backend.py +0 -0
  23. {fastws_cli-0.0.32 → fastws_cli-0.0.34}/fastws/news.py +0 -0
  24. {fastws_cli-0.0.32 → fastws_cli-0.0.34}/fastws/releases.py +0 -0
  25. {fastws_cli-0.0.32 → fastws_cli-0.0.34}/fastws_cli.egg-info/dependency_links.txt +0 -0
  26. {fastws_cli-0.0.32 → fastws_cli-0.0.34}/fastws_cli.egg-info/entry_points.txt +0 -0
  27. {fastws_cli-0.0.32 → fastws_cli-0.0.34}/fastws_cli.egg-info/requires.txt +0 -0
  28. {fastws_cli-0.0.32 → fastws_cli-0.0.34}/fastws_cli.egg-info/top_level.txt +0 -0
  29. {fastws_cli-0.0.32 → fastws_cli-0.0.34}/setup.cfg +0 -0
  30. {fastws_cli-0.0.32 → fastws_cli-0.0.34}/tests/test_build_backend.py +0 -0
  31. {fastws_cli-0.0.32 → fastws_cli-0.0.34}/tests/test_news.py +0 -0
  32. {fastws_cli-0.0.32 → fastws_cli-0.0.34}/tests/test_releases.py +0 -0
  33. {fastws_cli-0.0.32 → fastws_cli-0.0.34}/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.32
3
+ Version: 0.0.34
4
4
  Summary: Fast workspace tools for multi-repo management
5
5
  Author: Jeremy Howard
6
6
  License-Expression: Apache-2.0
@@ -200,14 +200,15 @@ ws-build --project solveit # only this package and its transitive workspace dep
200
200
 
201
201
  ### `cargo develop`
202
202
 
203
- Rebuild a mixed Rust/Python project's extension and replace it in the editable source package:
203
+ Build a mixed Rust/Python project's extension and opted-in binaries, keep them with its Python package as one build, and install that build in the editable source package and the venv:
204
204
 
205
205
  ```bash
206
206
  cargo develop
207
207
  cargo develop --path /path/to/project
208
+ cargo develop --profile quick
208
209
  ```
209
210
 
210
- Run from the project root, or give its directory with `--path`. The command reads `python-source`, `module-name`, `manifest-path` and features from `[tool.maturin]`. It always builds with Cargo's `test` profile, matching bare `cargo test`. The project must produce a `cdylib` and set `python-source`. To use release settings without command-line flags, add `[profile.test]` with `inherits = "release"` to `Cargo.toml`. Keep incremental compilation enabled for the local crate in that profile.
211
+ Run from the project root, or give its directory with `--path`. The command reads `python-source`, `module-name`, `manifest-path` and features from `[tool.maturin]`. `[tool.fastws] dev-profile = "quick"` selects a project's default for both `cargo develop` and `fastws.cargo.run_tests`. Without that setting they use `test`. An explicit `--profile NAME` overrides the project default. The project must produce a `cdylib` and set `python-source`. Define the selected profile in `Cargo.toml` and keep incremental compilation enabled for the local crate.
211
212
 
212
213
  Make the initial editable installation through `ws-sync` or maturin. Repeat that installation when package metadata or entry points change. Running Python processes keep the extension they loaded.
213
214
 
@@ -221,7 +222,13 @@ Set `native-binaries = true` under `[tool.fastws]` to build and install the bina
221
222
 
222
223
  Set `post-develop-module = "package.generator"` under `[tool.fastws]` to run a generator after installing the native extension and binaries, before copying wheel data. It runs with the current Python in a fresh process from the project root, including when Cargo reuses its build. A failure stops `cargo develop` or `ws-sync`. Use it for generated files that need the rebuilt extension's metadata.
223
224
 
224
- On Unix, each Cargo build that fastws runs holds a shared `flock` on `/tmp/aai-ws-timing.lock`. A speed test that takes that lock exclusively then never times code while a build runs. A build waits up to 5 minutes for such a timing to finish, then goes ahead without the lock. Python code takes the same lock with `fastws.cargo.timing_lock`: `with timing_lock(exclusive=True):` around a timing, and `with timing_lock():` around a test run. An exclusive hold that has waited 30 seconds prints the processes that have the lock's file open. It raises `TimeoutError` after 5 minutes. Windows builds and tests run without this timing coordination.
225
+ `cargo develop` keeps each build it installs in `~/.cache/fastws/builds/<project>/<id>/`, read-only. Files are copied first, then hashed, and hard-linked to blobs named by those captured bytes' sha256. The build id hashes these artifacts and the current digests of inputs listed by Cargo's dep-info. Input digests are observations at capture time, not proof of what the compiler read. The record's `started` gives the build's start time; `edited` flags inputs **possibly modified since build started**, by mtime, including inputs of a first build or newly discovered dependencies. Identical inputs and artifacts reuse their first capture's record.
226
+
227
+ The installed extension and binaries are symbolic links through `<project>/installed`, which `cargo develop` points at the new build in one step. `fastws.store.builds(root)` gives the newest build records, with input changes since the preceding build, and `fastws.store.resolve(root, id)` resolves a build-id prefix. Each develop deletes builds that are neither installed, among the project's last 20, nor from the last 2 days, then old unlinked blobs.
228
+
229
+ On Unix, cooperating `cargo develop` runs take a per-project lock through compilation, capture, installation, the post-develop generator and wheel data. Store readers and capture/install operations hold shared store access; collection takes it exclusively. Ordinary Cargo commands do not take the project lock: agents must not run competing raw Cargo builds against that project during develop. Windows installs copies and does not take these locks.
230
+
231
+ On Unix, every rustc call holds a shared `flock` on `/tmp/aai-ws-timing.lock`, through the rustc wrapper that sync writes (see `ws-sync`). A speed test that takes that lock exclusively stays apart from participating builds and test runs. A timing also holds `/tmp/aai-ws-timing.gate` while it waits for the lock and holds it, and a shared hold passes that file first, so new rustc calls and test runs wait behind the timing. Both shared and exclusive acquisition fail after 5 minutes; work never proceeds unlocked. Python code takes the same lock with `fastws.timelock.timing_lock`: `with timing_lock(exclusive=True):` around a timing, and `with timing_lock():` around a test run. Shared-inside-shared in one thread reuses its hold; each thread otherwise takes its own hold. Any nesting involving exclusive raises `RuntimeError` immediately. An exclusive hold that has waited 30 seconds prints the processes that have the lock's file open. Windows builds and tests run without this timing coordination.
225
232
 
226
233
  Use `fastws.build_backend` as the build backend of a mixed project, and include `fastws-cli>=0.0.29` alongside `maturin>=1.13,<2.0` in `[build-system].requires`. Its editable builds install only Python metadata and the source path, and compile no Rust. `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.
227
234
 
@@ -245,8 +252,51 @@ cargo wait
245
252
  cargo wait --path /path/to/workspace
246
253
  ```
247
254
 
248
- Use it when another process's unfinished edits stop the workspace compiling, such as a second agent working in the same tree. Each check runs `cargo check --all-targets` while holding the timing lock shared, as builds do. After a failed check, `cargo wait` prints Cargo's first error line. It checks again once a source file of a local package changes and then stays unchanged for 2 seconds. Local packages are the workspace's members and its path dependencies, such as a sibling crate that `ws-sync` patches in. When a check passes, it prints `compiles` and exits. After `--patience` seconds, 1800 by default, it prints the last check's errors and exits with status 1.
255
+ Use it when another process's unfinished edits stop the workspace compiling, such as a second agent working in the same tree. Each check runs `cargo check --all-targets`, whose rustc calls hold the timing lock shared, as every build's do. After a failed check, `cargo wait` prints Cargo's first error line. It checks again once a source file of a local package changes and then stays unchanged for 2 seconds. Local packages are the workspace's members and its path dependencies, such as a sibling crate that `ws-sync` patches in. When a check passes, it prints `compiles` and exits. After `--patience` seconds, 1800 by default, it prints the last check's errors and exits with status 1.
256
+
257
+ ### Rust builds and tests from Python
258
+
259
+ `fastws.cargo.run_tests` builds native tests once with Cargo, without a time limit, then runs the executables that Cargo reports directly from their package directories. Each binary gets its own `timeout`, 30 seconds by default, excluding compilation. A binary still running when its `timeout` ends is killed and counts as a failed binary, with its suite's `timed_out` set. Source edits between building and running do not trigger another build: the results describe the compiled build, not necessarily the latest working tree. It supports directly runnable native tests, not cross-target runners.
260
+
261
+ The default profile is `[tool.fastws].dev-profile`, or `test` when unset. Explicit `--profile NAME`, `--profile=NAME`, `--release` and `-r` override it. Pass Cargo build/selection options positionally, and test filters and harness options in `test_args`. A failed binary stops the run unless `--no-fail-fast` is given. Results contain compiler diagnostics and each binary's outcomes and captured failures.
262
+
263
+ ```python
264
+ from fastws.cargo import run_tests
265
+
266
+ native = run_tests('.', '--no-fail-fast')
267
+ focused = run_tests('.', '--test', 'core', test_args=('test_name', '--nocapture'))
268
+ ```
269
+
270
+ Doctests are deliberately not run; keep executable examples in ordinary Rust tests. `run_tests` accepts `env` overrides and `timeout=None` to disable its watchdog. It takes no timing lock around the run: rustc's wrapper owns its shared holds, and tests that time own their exclusive holds.
271
+
272
+ `fastws.cargo.check` runs any Cargo subcommand, `check --all-targets` by default, and returns the same `Results` without test suites. A failing run is a result, not an exception. Each `Diagnostic` in its `diagnostics` holds the message's path, line, column, level and text, and its error code or lint as `code`, such as `E0308` or `clippy::too_many_lines`. The summary lists errors first. `env` sets environment variables for the run, as for `run_tests`. A subcommand that rejects `--message-format`, such as `cargo llvm-lines`, runs with `diagnostics=False`: its result holds the exit status and output, with no diagnostics.
249
273
 
274
+ ```python
275
+ from fastws.cargo import check
276
+
277
+ lints = check('.', 'clippy', '--all-targets', '--', '-W', 'clippy::too_many_lines', env={'CLIPPY_CONF_DIR': 'review'})
278
+ errors = [d for d in lints.diagnostics if d.level == 'error']
279
+ sizes = check('.', 'llvm-lines', '--lib', diagnostics=False).text
280
+ ```
281
+
282
+ ### Integration snapshots
283
+
284
+ `fastws.integrate` builds several repos together while people or agents keep editing them. `integrate` snapshots each repo's working tree as a commit (untracked files included, ignored files not), checks each commit out as a detached worktree under one root, and writes the root's Cargo config. `HEAD` and the index of each live repo don't change. The config is the workspace's, with the patches for crates under the root pointing at the root's worktrees, so a build there reads the snapshots and every other local crate from the workspace. Keep the root outside the workspace, where `ws-sync` and uv don't treat its worktrees as members.
285
+
286
+ `errors` checks each snapshot in turn and keeps the errors in that repo's own files. A repo's check also reports its dependencies' errors, under paths outside the repo, and each dependency's own check already covers those. `owners` reads each owner's path globs from the bullets under a Markdown file's `Files` heading, one ``- Owner: `glob` `glob` `` line each, in order. `by_owner` groups each repo's compiler messages by owner, then by file, and the first owner whose globs match a path owns it. It prefixes each path with its repo's name, so a path into a sibling repo, such as `../rustymath/src/x.rs` from basedpl's check, groups as `rustymath/src/x.rs`. `run_tests` with `--no-fail-fast` runs every test binary, each within its own `timeout`, so a slow or crashing binary doesn't stop the others. `code_count` counts the Rust code lines in a sequence of lines, leaving out blanks and comments. `code_lines` applies it to each Rust file under a directory. `exhash`'s `Journal.stat(count=code_count)` counts the code lines that edits add and remove. `record` writes a run's messages, each repo's test `Results` with each failure's output, and any sizes, as a Markdown record, and returns each owner's message count.
287
+
288
+ ```python
289
+ from fastws.cargo import run_tests
290
+ from fastws.integrate import by_owner, code_lines, errors, integrate, owners, record
291
+
292
+ root = '~/git/integration'
293
+ commits = integrate({'basedpl': '~/aai-ws/basedpl', 'rustymath': '~/aai-ws/rustymath'}, root)
294
+ errs = errors(root, ['rustymath', 'basedpl'])
295
+ groups = by_owner(errs, owners('~/aai-ws/basedpl/meta/owners.md'))
296
+ tests = {r: run_tests(f'{root}/{r}', '--no-fail-fast') for r in errs if not errs[r]}
297
+ sizes = {r: code_lines(f'{root}/{r}/src') for r in errs}
298
+ counts = record(f'{root}/run8.md', 'Run 8', commits, groups, tests=tests, sizes=sizes)
299
+ ```
250
300
  ### `ws-sync`
251
301
 
252
302
  Syncing proceeds in this order:
@@ -266,9 +316,13 @@ Existing globs, entries for missing directories, and entries for checkouts that
266
316
 
267
317
  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.
268
318
 
319
+ Sync also defaults `[term] quiet = true` in that file to suppress routine Cargo output and warnings. An explicit `quiet = false` is preserved.
320
+
269
321
  Do not commit a `Cargo.lock` generated under these patches. Its source-less local entries cannot be resolved on another machine.
270
322
 
271
- When `sccache` is installed, sync also configures it as Cargo's `rustc-wrapper`, allowing unchanged compilation units to be reused across the workspace's otherwise-independent Cargo target directories. An existing wrapper is always preserved. If neither a wrapper nor `sccache` is present, sync warns with the install command (`cargo install sccache --locked`) and continues; rerun `ws-sync` after installing to enable it.
323
+ Sync writes `.cargo/rustc-lock` and makes it Cargo's `rustc-wrapper`. The script runs each rustc call holding the timing lock shared (see `cargo develop` above). Inside the lock it runs the wrapper that the config named before sync first wrote the script, where another tool set one, and otherwise `sccache` when it's installed, so unchanged compilation units are reused across the workspace's otherwise-independent Cargo target directories. Later syncs keep that inner wrapper. The script costs about 9 ms a rustc call, mostly the Python interpreter's start. If `sccache` isn't installed, sync warns with the install command (`cargo install sccache --locked`) and continues; rerun `ws-sync` after installing to enable it. On Windows, where the lock does nothing, the config keeps another tool's wrapper, or else names `sccache`.
324
+
325
+ For an isolated build workspace, `fastws.core.cargo_setup(root)` regenerates these Cargo patches and the same compiler wrapper without pulling, syncing Python or building.
272
326
 
273
327
  Fastws discovers JavaScript packages directly under the workspace root. It reads each package's `workspaces` list for declared nested packages. These lists accept paths and globs. Fastws ignores undeclared nested packages. Lockfiles do not affect discovery. `[tool.fastws].exclude` matches paths relative to the workspace root. Fastws skips `node_modules` and directories whose names start with `.` or `_`.
274
328
 
@@ -172,14 +172,15 @@ ws-build --project solveit # only this package and its transitive workspace dep
172
172
 
173
173
  ### `cargo develop`
174
174
 
175
- Rebuild a mixed Rust/Python project's extension and replace it in the editable source package:
175
+ Build a mixed Rust/Python project's extension and opted-in binaries, keep them with its Python package as one build, and install that build in the editable source package and the venv:
176
176
 
177
177
  ```bash
178
178
  cargo develop
179
179
  cargo develop --path /path/to/project
180
+ cargo develop --profile quick
180
181
  ```
181
182
 
182
- Run from the project root, or give its directory with `--path`. The command reads `python-source`, `module-name`, `manifest-path` and features from `[tool.maturin]`. It always builds with Cargo's `test` profile, matching bare `cargo test`. The project must produce a `cdylib` and set `python-source`. To use release settings without command-line flags, add `[profile.test]` with `inherits = "release"` to `Cargo.toml`. Keep incremental compilation enabled for the local crate in that profile.
183
+ Run from the project root, or give its directory with `--path`. The command reads `python-source`, `module-name`, `manifest-path` and features from `[tool.maturin]`. `[tool.fastws] dev-profile = "quick"` selects a project's default for both `cargo develop` and `fastws.cargo.run_tests`. Without that setting they use `test`. An explicit `--profile NAME` overrides the project default. The project must produce a `cdylib` and set `python-source`. Define the selected profile in `Cargo.toml` and keep incremental compilation enabled for the local crate.
183
184
 
184
185
  Make the initial editable installation through `ws-sync` or maturin. Repeat that installation when package metadata or entry points change. Running Python processes keep the extension they loaded.
185
186
 
@@ -193,7 +194,13 @@ Set `native-binaries = true` under `[tool.fastws]` to build and install the bina
193
194
 
194
195
  Set `post-develop-module = "package.generator"` under `[tool.fastws]` to run a generator after installing the native extension and binaries, before copying wheel data. It runs with the current Python in a fresh process from the project root, including when Cargo reuses its build. A failure stops `cargo develop` or `ws-sync`. Use it for generated files that need the rebuilt extension's metadata.
195
196
 
196
- On Unix, each Cargo build that fastws runs holds a shared `flock` on `/tmp/aai-ws-timing.lock`. A speed test that takes that lock exclusively then never times code while a build runs. A build waits up to 5 minutes for such a timing to finish, then goes ahead without the lock. Python code takes the same lock with `fastws.cargo.timing_lock`: `with timing_lock(exclusive=True):` around a timing, and `with timing_lock():` around a test run. An exclusive hold that has waited 30 seconds prints the processes that have the lock's file open. It raises `TimeoutError` after 5 minutes. Windows builds and tests run without this timing coordination.
197
+ `cargo develop` keeps each build it installs in `~/.cache/fastws/builds/<project>/<id>/`, read-only. Files are copied first, then hashed, and hard-linked to blobs named by those captured bytes' sha256. The build id hashes these artifacts and the current digests of inputs listed by Cargo's dep-info. Input digests are observations at capture time, not proof of what the compiler read. The record's `started` gives the build's start time; `edited` flags inputs **possibly modified since build started**, by mtime, including inputs of a first build or newly discovered dependencies. Identical inputs and artifacts reuse their first capture's record.
198
+
199
+ The installed extension and binaries are symbolic links through `<project>/installed`, which `cargo develop` points at the new build in one step. `fastws.store.builds(root)` gives the newest build records, with input changes since the preceding build, and `fastws.store.resolve(root, id)` resolves a build-id prefix. Each develop deletes builds that are neither installed, among the project's last 20, nor from the last 2 days, then old unlinked blobs.
200
+
201
+ On Unix, cooperating `cargo develop` runs take a per-project lock through compilation, capture, installation, the post-develop generator and wheel data. Store readers and capture/install operations hold shared store access; collection takes it exclusively. Ordinary Cargo commands do not take the project lock: agents must not run competing raw Cargo builds against that project during develop. Windows installs copies and does not take these locks.
202
+
203
+ On Unix, every rustc call holds a shared `flock` on `/tmp/aai-ws-timing.lock`, through the rustc wrapper that sync writes (see `ws-sync`). A speed test that takes that lock exclusively stays apart from participating builds and test runs. A timing also holds `/tmp/aai-ws-timing.gate` while it waits for the lock and holds it, and a shared hold passes that file first, so new rustc calls and test runs wait behind the timing. Both shared and exclusive acquisition fail after 5 minutes; work never proceeds unlocked. Python code takes the same lock with `fastws.timelock.timing_lock`: `with timing_lock(exclusive=True):` around a timing, and `with timing_lock():` around a test run. Shared-inside-shared in one thread reuses its hold; each thread otherwise takes its own hold. Any nesting involving exclusive raises `RuntimeError` immediately. An exclusive hold that has waited 30 seconds prints the processes that have the lock's file open. Windows builds and tests run without this timing coordination.
197
204
 
198
205
  Use `fastws.build_backend` as the build backend of a mixed project, and include `fastws-cli>=0.0.29` alongside `maturin>=1.13,<2.0` in `[build-system].requires`. Its editable builds install only Python metadata and the source path, and compile no Rust. `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.
199
206
 
@@ -217,8 +224,51 @@ cargo wait
217
224
  cargo wait --path /path/to/workspace
218
225
  ```
219
226
 
220
- Use it when another process's unfinished edits stop the workspace compiling, such as a second agent working in the same tree. Each check runs `cargo check --all-targets` while holding the timing lock shared, as builds do. After a failed check, `cargo wait` prints Cargo's first error line. It checks again once a source file of a local package changes and then stays unchanged for 2 seconds. Local packages are the workspace's members and its path dependencies, such as a sibling crate that `ws-sync` patches in. When a check passes, it prints `compiles` and exits. After `--patience` seconds, 1800 by default, it prints the last check's errors and exits with status 1.
227
+ Use it when another process's unfinished edits stop the workspace compiling, such as a second agent working in the same tree. Each check runs `cargo check --all-targets`, whose rustc calls hold the timing lock shared, as every build's do. After a failed check, `cargo wait` prints Cargo's first error line. It checks again once a source file of a local package changes and then stays unchanged for 2 seconds. Local packages are the workspace's members and its path dependencies, such as a sibling crate that `ws-sync` patches in. When a check passes, it prints `compiles` and exits. After `--patience` seconds, 1800 by default, it prints the last check's errors and exits with status 1.
228
+
229
+ ### Rust builds and tests from Python
230
+
231
+ `fastws.cargo.run_tests` builds native tests once with Cargo, without a time limit, then runs the executables that Cargo reports directly from their package directories. Each binary gets its own `timeout`, 30 seconds by default, excluding compilation. A binary still running when its `timeout` ends is killed and counts as a failed binary, with its suite's `timed_out` set. Source edits between building and running do not trigger another build: the results describe the compiled build, not necessarily the latest working tree. It supports directly runnable native tests, not cross-target runners.
232
+
233
+ The default profile is `[tool.fastws].dev-profile`, or `test` when unset. Explicit `--profile NAME`, `--profile=NAME`, `--release` and `-r` override it. Pass Cargo build/selection options positionally, and test filters and harness options in `test_args`. A failed binary stops the run unless `--no-fail-fast` is given. Results contain compiler diagnostics and each binary's outcomes and captured failures.
234
+
235
+ ```python
236
+ from fastws.cargo import run_tests
237
+
238
+ native = run_tests('.', '--no-fail-fast')
239
+ focused = run_tests('.', '--test', 'core', test_args=('test_name', '--nocapture'))
240
+ ```
241
+
242
+ Doctests are deliberately not run; keep executable examples in ordinary Rust tests. `run_tests` accepts `env` overrides and `timeout=None` to disable its watchdog. It takes no timing lock around the run: rustc's wrapper owns its shared holds, and tests that time own their exclusive holds.
243
+
244
+ `fastws.cargo.check` runs any Cargo subcommand, `check --all-targets` by default, and returns the same `Results` without test suites. A failing run is a result, not an exception. Each `Diagnostic` in its `diagnostics` holds the message's path, line, column, level and text, and its error code or lint as `code`, such as `E0308` or `clippy::too_many_lines`. The summary lists errors first. `env` sets environment variables for the run, as for `run_tests`. A subcommand that rejects `--message-format`, such as `cargo llvm-lines`, runs with `diagnostics=False`: its result holds the exit status and output, with no diagnostics.
221
245
 
246
+ ```python
247
+ from fastws.cargo import check
248
+
249
+ lints = check('.', 'clippy', '--all-targets', '--', '-W', 'clippy::too_many_lines', env={'CLIPPY_CONF_DIR': 'review'})
250
+ errors = [d for d in lints.diagnostics if d.level == 'error']
251
+ sizes = check('.', 'llvm-lines', '--lib', diagnostics=False).text
252
+ ```
253
+
254
+ ### Integration snapshots
255
+
256
+ `fastws.integrate` builds several repos together while people or agents keep editing them. `integrate` snapshots each repo's working tree as a commit (untracked files included, ignored files not), checks each commit out as a detached worktree under one root, and writes the root's Cargo config. `HEAD` and the index of each live repo don't change. The config is the workspace's, with the patches for crates under the root pointing at the root's worktrees, so a build there reads the snapshots and every other local crate from the workspace. Keep the root outside the workspace, where `ws-sync` and uv don't treat its worktrees as members.
257
+
258
+ `errors` checks each snapshot in turn and keeps the errors in that repo's own files. A repo's check also reports its dependencies' errors, under paths outside the repo, and each dependency's own check already covers those. `owners` reads each owner's path globs from the bullets under a Markdown file's `Files` heading, one ``- Owner: `glob` `glob` `` line each, in order. `by_owner` groups each repo's compiler messages by owner, then by file, and the first owner whose globs match a path owns it. It prefixes each path with its repo's name, so a path into a sibling repo, such as `../rustymath/src/x.rs` from basedpl's check, groups as `rustymath/src/x.rs`. `run_tests` with `--no-fail-fast` runs every test binary, each within its own `timeout`, so a slow or crashing binary doesn't stop the others. `code_count` counts the Rust code lines in a sequence of lines, leaving out blanks and comments. `code_lines` applies it to each Rust file under a directory. `exhash`'s `Journal.stat(count=code_count)` counts the code lines that edits add and remove. `record` writes a run's messages, each repo's test `Results` with each failure's output, and any sizes, as a Markdown record, and returns each owner's message count.
259
+
260
+ ```python
261
+ from fastws.cargo import run_tests
262
+ from fastws.integrate import by_owner, code_lines, errors, integrate, owners, record
263
+
264
+ root = '~/git/integration'
265
+ commits = integrate({'basedpl': '~/aai-ws/basedpl', 'rustymath': '~/aai-ws/rustymath'}, root)
266
+ errs = errors(root, ['rustymath', 'basedpl'])
267
+ groups = by_owner(errs, owners('~/aai-ws/basedpl/meta/owners.md'))
268
+ tests = {r: run_tests(f'{root}/{r}', '--no-fail-fast') for r in errs if not errs[r]}
269
+ sizes = {r: code_lines(f'{root}/{r}/src') for r in errs}
270
+ counts = record(f'{root}/run8.md', 'Run 8', commits, groups, tests=tests, sizes=sizes)
271
+ ```
222
272
  ### `ws-sync`
223
273
 
224
274
  Syncing proceeds in this order:
@@ -238,9 +288,13 @@ Existing globs, entries for missing directories, and entries for checkouts that
238
288
 
239
289
  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.
240
290
 
291
+ Sync also defaults `[term] quiet = true` in that file to suppress routine Cargo output and warnings. An explicit `quiet = false` is preserved.
292
+
241
293
  Do not commit a `Cargo.lock` generated under these patches. Its source-less local entries cannot be resolved on another machine.
242
294
 
243
- When `sccache` is installed, sync also configures it as Cargo's `rustc-wrapper`, allowing unchanged compilation units to be reused across the workspace's otherwise-independent Cargo target directories. An existing wrapper is always preserved. If neither a wrapper nor `sccache` is present, sync warns with the install command (`cargo install sccache --locked`) and continues; rerun `ws-sync` after installing to enable it.
295
+ Sync writes `.cargo/rustc-lock` and makes it Cargo's `rustc-wrapper`. The script runs each rustc call holding the timing lock shared (see `cargo develop` above). Inside the lock it runs the wrapper that the config named before sync first wrote the script, where another tool set one, and otherwise `sccache` when it's installed, so unchanged compilation units are reused across the workspace's otherwise-independent Cargo target directories. Later syncs keep that inner wrapper. The script costs about 9 ms a rustc call, mostly the Python interpreter's start. If `sccache` isn't installed, sync warns with the install command (`cargo install sccache --locked`) and continues; rerun `ws-sync` after installing to enable it. On Windows, where the lock does nothing, the config keeps another tool's wrapper, or else names `sccache`.
296
+
297
+ For an isolated build workspace, `fastws.core.cargo_setup(root)` regenerates these Cargo patches and the same compiler wrapper without pulling, syncing Python or building.
244
298
 
245
299
  Fastws discovers JavaScript packages directly under the workspace root. It reads each package's `workspaces` list for declared nested packages. These lists accept paths and globs. Fastws ignores undeclared nested packages. Lockfiles do not affect discovery. `[tool.fastws].exclude` matches paths relative to the workspace root. Fastws skips `node_modules` and directories whose names start with `.` or `_`.
246
300
 
@@ -1,4 +1,6 @@
1
- __version__ = "0.0.32"
1
+ __version__ = "0.0.34"
2
+
3
+
2
4
 
3
5
 
4
6
 
@@ -0,0 +1,4 @@
1
+ __all__ = ['tomllib']
2
+
3
+ try: import tomllib
4
+ except ModuleNotFoundError: import tomli as tomllib
@@ -0,0 +1,292 @@
1
+ r"Build, test and install Cargo artifacts for mixed Rust/Python projects."
2
+ import json, os, re, shutil, signal, subprocess, sys, sysconfig, time
3
+ from collections import Counter
4
+ from dataclasses import dataclass, field
5
+ from pathlib import Path
6
+ from typing import NamedTuple
7
+ from . import store
8
+ from .procs import bounded
9
+ from ._toml import tomllib
10
+
11
+
12
+ class ConfigError(ValueError):
13
+ "A project's `pyproject.toml` lacks a setting that a command needs."
14
+
15
+
16
+ def settings(root):
17
+ "Read the project's tool configuration."
18
+ return tomllib.loads((Path(root).expanduser()/'pyproject.toml').read_text()).get('tool', {})
19
+
20
+
21
+ def _dev_profile(root):
22
+ return settings(root).get('fastws', {}).get('dev-profile', 'test') if (root/'pyproject.toml').exists() else 'test'
23
+
24
+
25
+ def cargo(root, *args):
26
+ "Run Cargo in the project and return its standard output. A failure raises `CalledProcessError`, holding Cargo's standard error."
27
+ return subprocess.run(['cargo', *args], cwd=Path(root).expanduser(), capture_output=True, text=True, check=True).stdout
28
+
29
+
30
+ def members(root):
31
+ "The manifests of the workspace's member packages."
32
+ return {Path(p['manifest_path']) for p in json.loads(cargo(root, 'metadata', '--no-deps', '--format-version', '1'))['packages']}
33
+
34
+
35
+ _TEST = re.compile(r'^test (.+) \.\.\. (ok|FAILED|ignored)')
36
+ _FAILURE = re.compile(r'^---- (.+) std(?:out|err) ----$')
37
+
38
+
39
+ class Diagnostic(NamedTuple):
40
+ "A compiler message at its primary span, read from Cargo's JSON messages."
41
+ path:str; line:int; col:int; level:str; message:str
42
+ code:str|None=None # The error code or lint, such as `E0308` or `clippy::too_many_lines`
43
+ def __str__(self): return f'{self.level}{f"[{self.code}]" if self.code else ""}: {self.path}:{self.line}: {self.message}'
44
+
45
+
46
+ def _run(root, args, env=None, diagnostics=True):
47
+ "Run the Cargo subcommand and options `args` in `root`, with the environment variables `env` set: its exit status, its output, each `Diagnostic` once, and Cargo's other JSON messages, such as artifacts. With `diagnostics`, Cargo writes JSON messages and the output renders each compiler message short; without, the output is as Cargo printed it."
48
+ flags = ('--message-format=json-diagnostic-short',) if diagnostics else ()
49
+ p = bounded(['cargo', args[0], *flags, *args[1:]], seconds=None, merge=True, cwd=root, env=os.environ | (env or {}))
50
+ if not diagnostics: return p.returncode, p.stdout, [], []
51
+ text, found, rest = [], [], []
52
+ for line in p.stdout.splitlines(keepends=True):
53
+ if not line.startswith('{'): text.append(line); continue
54
+ m = json.loads(line)
55
+ if m['reason'] != 'compiler-message': rest.append(m); continue
56
+ msg = m['message']
57
+ text.append(msg['rendered'])
58
+ if s := next((s for s in msg['spans'] if s['is_primary']), None):
59
+ found.append(Diagnostic(s['file_name'], s['line_start'], s['column_start'], msg['level'], msg['message'], (msg['code'] or {}).get('code')))
60
+ return p.returncode, ''.join(text), list(dict.fromkeys(found)), rest
61
+
62
+
63
+ def _lines(text, n=20, tail=False):
64
+ "Up to `n` lines of `text` (None: all), indented, from its end if `tail`, with a count of the lines left out."
65
+ lines = text.splitlines()
66
+ if n is None or len(lines) <= n: return [f' {l}' for l in lines]
67
+ shown = [f' {l}' for l in (lines[-n:] if tail else lines[:n])]
68
+ more = f' … {len(lines)-n} more lines'
69
+ return [more, *shown] if tail else [*shown, more]
70
+
71
+
72
+ @dataclass
73
+ class Suite:
74
+ "What one native test binary printed."
75
+ name:str # The target source, such as `tests/core.rs` or `unittests src/lib.rs`
76
+ output:str # Everything the binary printed, standard output and standard error merged
77
+ tests:dict # Each test's outcome: `ok`, `FAILED` or `ignored`
78
+ failures:dict # Each failing test's captured output
79
+ summary:str # libtest's `test result:` line, empty for a binary that crashed or has its own harness
80
+ failed:bool # Did the binary exit unsuccessfully?
81
+ timed_out:bool=False # Was the binary killed for running out of time?
82
+
83
+ @classmethod
84
+ def read(cls, name, lines, returncode, timed_out=False):
85
+ "Read one binary's lines and exit status, and whether it was killed for running out of time."
86
+ failures, current, in_failures = {}, None, False
87
+ for line in lines:
88
+ if line in ('successes:', 'failures:'): current, in_failures = None, line == 'failures:'
89
+ elif in_failures and (m := _FAILURE.match(line)): current = failures.setdefault(m[1], [])
90
+ elif current is not None: current.append(line)
91
+ return cls(name, '\n'.join(lines), {m[1]: m[2] for l in lines if (m := _TEST.match(l))},
92
+ {k: '\n'.join(v).strip('\n') for k,v in failures.items()},
93
+ next((l for l in lines if l.startswith('test result: ')), ''), bool(returncode), timed_out)
94
+
95
+ def __str__(self):
96
+ n = Counter(self.tests.values())
97
+ status = ', '.join(f'{n[o]} {w}' for o,w in [('ok', 'passed'), ('FAILED', 'failed'), ('ignored', 'ignored')] if n[o])
98
+ if self.failed and not n['FAILED']:
99
+ end = 'ran out of time' if self.timed_out else 'failed'
100
+ status = f'{status}, then {end}' if status else end
101
+ time = f' in {m[1]}' if (m := re.search(r'finished in (\S+)$', self.summary)) else ''
102
+ return f'{self.name}: {status or "ok"}{time}'
103
+
104
+
105
+ @dataclass
106
+ class Results:
107
+ "A Cargo run's outcome, its compiler messages and, for tests, each native test binary's results. It displays as a summary: the outcome, the compiler messages with errors first, each binary that ran tests, and each failure's output."
108
+ command:str # The Cargo subcommand, such as `test` or `clippy`
109
+ returncode:int
110
+ text:str # The build's messages and each binary's merged standard output and standard error
111
+ diagnostics:list # Each `Diagnostic` from the build, before the first binary ran
112
+ suites:list=field(default_factory=list) # Each test binary's `Suite`, in execution order
113
+ seconds:float=0 # Wall time spent executing native test binaries, excluding compilation
114
+
115
+ @property
116
+ def ok(self): return self.returncode == 0
117
+
118
+ def summary(self, lines:int|None=20):
119
+ "The outcome, compiler messages, each binary's result and each failure's output, with up to `lines` lines of each output (None: all)"
120
+ res = [f'cargo {self.command} passed' if self.ok else f'cargo {self.command} failed with exit status {self.returncode}']
121
+ diags = sorted(self.diagnostics, key=lambda d: d.level != 'error')
122
+ res += [str(d) for d in diags[:lines]]
123
+ if lines is not None and len(diags) > lines: res.append(f'{len(diags)-lines} more compiler messages in `diagnostics`')
124
+ ran = [s for s in self.suites if s.tests or not s.summary]
125
+ for s in ran:
126
+ res.append(str(s))
127
+ for t,out in s.failures.items(): res += [f' FAILED {t}', *_lines(out, lines)]
128
+ if s.failed and not s.failures: res += _lines(s.output, lines, tail=True)
129
+ if idle := len(self.suites) - len(ran): res.append(f'{idle} test binaries ran no tests')
130
+ if not (self.ok or self.suites or diags): res += _lines(self.text, lines, tail=True)
131
+ return '\n'.join(res)
132
+ __repr__ = summary
133
+
134
+
135
+ def check(
136
+ root, # A Cargo package or workspace (expands `~`)
137
+ *args, # The Cargo subcommand and its options, such as `'clippy', '--all-targets', '--', '-W', 'clippy::pedantic'`; none: `check --all-targets`
138
+ env:dict=None, # Environment variables to set for the run, such as `{'CLIPPY_CONF_DIR': '/path/to/review'}`
139
+ diagnostics:bool=True, # Ask Cargo for JSON compiler messages? False runs a subcommand that rejects `--message-format`, such as `llvm-lines`, with no diagnostics
140
+ )->Results:
141
+ "Run a Cargo subcommand and return its `Results`, with each compiler message as a `Diagnostic`. A failing run is a result, not an exception."
142
+ args = args or ('check', '--all-targets')
143
+ return Results(args[0], *_run(Path(root).expanduser(), args, env, diagnostics)[:3])
144
+
145
+
146
+ def run_tests(
147
+ root, # A Cargo package or workspace (expands `~`)
148
+ *args, # Cargo build/selection options, such as `'--test', 'core'` or `'--profile', 'checked'`; `--no-fail-fast` runs every binary
149
+ test_args=(), # Arguments passed directly to each test binary, such as `('name', '--nocapture')`
150
+ env:dict=None, # Environment variables to set for the run, such as `{'BASEDPL_COSTS': '1'}`
151
+ timeout:float=30 # Seconds that each native test binary may run after building; None: no limit
152
+ )->Results:
153
+ "Build native tests once, with no time limit, then run Cargo's reported executables directly from their package directories. Source edits after building cannot trigger another build; results describe the compiled build, not the latest working tree. `timeout` bounds each binary on its own, excluding compilation. A binary that runs out of time is killed and reported as a failed binary whose suite is `timed_out`. An omitted profile uses tool.fastws.dev-profile, or test. Doctests are not run. This takes no timing lock: rustc's wrapper and tests that time own their holds."
154
+ root, env = Path(root).expanduser().resolve(), {'CARGO_TERM_QUIET': 'false', 'CARGO_TERM_COLOR': 'never'} | (env or {})
155
+ has_profile = any(a in ('-r', '--release', '--profile') or isinstance(a, str) and a.startswith('--profile=') for a in args)
156
+ if not has_profile: args += ('--profile', _dev_profile(root))
157
+ code, text, found, rest = _run(root, ('test', '--no-run', *args), env)
158
+ if code: return Results('test', code, text, found)
159
+ artifacts = [m for m in rest if m['reason'] == 'compiler-artifact' and m['profile']['test'] and m.get('executable')]
160
+ suites, code, start = [], 0, time.monotonic()
161
+ for m in artifacts:
162
+ package = Path(m['manifest_path']).parent
163
+ name = Path(m['target']['src_path']).relative_to(package).as_posix()
164
+ if 'test' not in m['target']['kind']: name = 'unittests ' + name
165
+ try: p, late = bounded([m['executable'], *test_args], timeout, merge=True, cwd=package, env=os.environ | env | {'CARGO_MANIFEST_DIR': str(package)}), False
166
+ except TimeoutError as e: p, late = subprocess.CompletedProcess(m['executable'], -signal.SIGKILL, f'{e}\n'), True
167
+ suites.append(Suite.read(name, p.stdout.splitlines(), p.returncode, late))
168
+ text += f' Running {name} ({m["executable"]})\n' + p.stdout
169
+ if p.returncode:
170
+ code = p.returncode
171
+ if '--no-fail-fast' not in args: break
172
+ return Results('test', code, text, found, suites, time.monotonic() - start)
173
+
174
+
175
+ def sources(root):
176
+ "The manifests, and the target directories or files, of the local packages that the workspace builds: its members and its path dependencies."
177
+ res = set()
178
+ for p in json.loads(cargo(root, 'metadata', '--format-version', '1'))['packages']:
179
+ if p['source'] is not None: continue
180
+ manifest = Path(p['manifest_path'])
181
+ res.add(manifest)
182
+ for t in p['targets']:
183
+ src = Path(t['src_path'])
184
+ res.add(src if src.parent == manifest.parent else src.parent)
185
+ return res
186
+
187
+
188
+ def _maturin_flags(target, env):
189
+ "Maturin's macOS linker flags without replacing existing compiler flags."
190
+ target = target or env.get('CARGO_BUILD_TARGET')
191
+ if not (target.endswith('-apple-darwin') if target else sys.platform == 'darwin'): return [], None
192
+ link_args = ['link-arg=-undefined', 'link-arg=dynamic_lookup']
193
+ if 'CARGO_ENCODED_RUSTFLAGS' in env:
194
+ flags = env['CARGO_ENCODED_RUSTFLAGS'].split('\x1f') if env['CARGO_ENCODED_RUSTFLAGS'] else []
195
+ elif 'RUSTFLAGS' in env: flags = env['RUSTFLAGS'].split()
196
+ else:
197
+ flags = [arg for flag in link_args for arg in ('-C', flag)]
198
+ return ['--config', 'build.rustflags=' + json.dumps(flags)], None
199
+ for flag in link_args:
200
+ if flag not in flags and '-C'+flag not in flags: flags += ['-C', flag]
201
+ return [], dict(env, CARGO_ENCODED_RUSTFLAGS='\x1f'.join(flags))
202
+
203
+
204
+ def build(
205
+ root, # The project directory, whose `tool.maturin` gives the defaults (expands `~`)
206
+ *,
207
+ library=True, # Build the library target?
208
+ binaries=False, # Build the binary targets?
209
+ profile=None, # Cargo profile; None: `tool.maturin.profile`, or Cargo's default
210
+ target=None, # Target triple; None: the host
211
+ features=None, # Features to enable, as a list or a string; None: `tool.maturin.features`
212
+ all_features=False, # Enable every feature? `tool.maturin`'s `all-features` also turns this on
213
+ no_default_features=False, # Leave out the default features? `tool.maturin`'s `no-default-features` also turns this on
214
+ for_maturin=False, # Use the macOS linker flags that maturin's own wheel build uses?
215
+ ):
216
+ "Build with Cargo and return the compiler artifacts of the workspace's packages."
217
+ root = Path(root).expanduser()
218
+ cfg = settings(root)['maturin']
219
+ cmd = ['build', '--manifest-path', str((root/'Cargo.toml').resolve()), '--message-format=json-diagnostic-short']
220
+ if library: cmd.append('--lib')
221
+ if binaries: cmd.append('--bins')
222
+ if profile := profile or cfg.get('profile'): cmd += ['--profile', profile]
223
+ if target: cmd += ['--target', target]
224
+ if features := features or cfg.get('features'):
225
+ cmd += ['--features', features if isinstance(features, str) else ','.join(features)]
226
+ for flag, active in [('all-features', all_features), ('no-default-features', no_default_features)]:
227
+ if active or cfg.get(flag): cmd.append('--' + flag)
228
+ extra, env = _maturin_flags(target, os.environ) if for_maturin else ([], None)
229
+ result = subprocess.run(['cargo', *cmd, *extra], cwd=root, stdout=subprocess.PIPE, text=True, env=env)
230
+ messages = [json.loads(line) for line in result.stdout.splitlines()]
231
+ for m in messages:
232
+ if m.get('reason') != 'compiler-message': continue
233
+ name = tomllib.loads(Path(m['manifest_path']).read_text())['package']['name']
234
+ for line in m['message']['rendered'].splitlines(): print(f'[{name}] {line}', file=sys.stderr)
235
+ result.check_returncode()
236
+ workspace = members(root)
237
+ return [m for m in messages if m.get('reason') == 'compiler-artifact' and Path(m['manifest_path']) in workspace]
238
+
239
+
240
+ def replace(source, destination):
241
+ "Replace an installed artifact without changing the file held by running processes."
242
+ destination.parent.mkdir(parents=True, exist_ok=True)
243
+ tmp = destination.with_name(f'{destination.name}.{os.getpid()}.tmp')
244
+ shutil.copy2(source, tmp)
245
+ os.replace(tmp, destination)
246
+ return destination
247
+
248
+
249
+ def develop(root, *, profile=None):
250
+ "Serialize this project's cooperating develop runs through build, artifact capture, installation, generator and wheel data. An omitted profile uses tool.fastws.dev-profile, or test. Raw Cargo commands do not take this lock."
251
+ root = Path(root).expanduser()
252
+ with store.building(root) as started:
253
+ tool = settings(root)
254
+ cfg = tool['maturin']
255
+ if 'python-source' not in cfg: raise ConfigError(r'cargo develop requires tool.maturin.python-source')
256
+ binaries = tool.get('fastws', {}).get('native-binaries', False)
257
+ artifacts = build(root, binaries=binaries, profile=profile or _dev_profile(root))
258
+ artifact, = [m for m in artifacts if 'cdylib' in m['target']['crate_types']]
259
+ lib, = [Path(f) for f in artifact['filenames'] if Path(f).suffix in ('.dylib', '.so', '.dll')]
260
+ module = cfg.get('module-name', artifact['target']['name'])
261
+ src = root/cfg['python-source']
262
+ ext = src/Path(*module.split('.'))
263
+ ext = ext.with_name(ext.name + sysconfig.get_config_var('EXT_SUFFIX'))
264
+ env = Path(os.environ.get('VIRTUAL_ENV', sys.prefix))
265
+ scripts = env/('Scripts' if os.name == 'nt' else 'bin')
266
+ exes = [Path(m['executable']) for m in artifacts if binaries and m.get('executable')]
267
+ package = src/module.split('.')[0]
268
+ files = {f'python/{f.relative_to(src)}': f for f in package.rglob('*') if f.is_file() and f != ext and '__pycache__' not in f.parts}
269
+ files |= {f'python/{ext.relative_to(src)}': lib} | {f'bin/{e.name}': e for e in exes}
270
+ with store.access():
271
+ kept = store.keep(root, files, dict.fromkeys(f for a in [lib, *exes] for f in store.dep_files(a)), started)
272
+ store.install(root, kept, {ext: f'python/{ext.relative_to(src)}'} | {scripts/e.name: f'bin/{e.name}' for e in exes})
273
+ store.collect()
274
+ if generator := tool.get('fastws', {}).get('post-develop-module'):
275
+ subprocess.run([sys.executable, '-m', generator], cwd=root, check=True)
276
+ # Editable builds through `build_backend` don't install the wheel's data files, which a wheel install puts under the prefix.
277
+ if 'data' in cfg:
278
+ data = root/cfg['data']/'data'
279
+ for f in data.rglob('*'):
280
+ if f.is_file(): replace(f, env/f.relative_to(data))
281
+
282
+
283
+ def stage_binaries(root, profile=None, target=None, features=None, all_features=False, no_default_features=False):
284
+ "Stage Python-free Cargo binaries for a maturin wheel build."
285
+ root = Path(root).expanduser()
286
+ cfg = settings(root)['maturin']
287
+ if 'data' not in cfg: raise ConfigError(r'staging native binaries requires tool.maturin.data')
288
+ dest = root/cfg['data']/'scripts'
289
+ artifacts = build(root, library=False, binaries=True, profile=profile, target=target, features=features,
290
+ all_features=all_features, no_default_features=no_default_features, for_maturin=True)
291
+ if dest.exists(): shutil.rmtree(dest)
292
+ return [replace(m['executable'], dest/Path(m['executable']).name) for m in artifacts if m.get('executable')]