fastws-cli 0.0.33__tar.gz → 0.0.35__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 (34) hide show
  1. {fastws_cli-0.0.33 → fastws_cli-0.0.35}/PKG-INFO +42 -6
  2. {fastws_cli-0.0.33 → fastws_cli-0.0.35}/README.md +41 -5
  3. {fastws_cli-0.0.33 → fastws_cli-0.0.35}/fastws/__init__.py +3 -1
  4. fastws_cli-0.0.35/fastws/_toml.py +4 -0
  5. fastws_cli-0.0.35/fastws/cargo.py +377 -0
  6. {fastws_cli-0.0.33 → fastws_cli-0.0.35}/fastws/core.py +54 -28
  7. fastws_cli-0.0.35/fastws/integrate.py +136 -0
  8. {fastws_cli-0.0.33 → fastws_cli-0.0.35}/fastws/store.py +6 -2
  9. {fastws_cli-0.0.33 → fastws_cli-0.0.35}/fastws_cli.egg-info/PKG-INFO +42 -6
  10. {fastws_cli-0.0.33 → fastws_cli-0.0.35}/fastws_cli.egg-info/SOURCES.txt +3 -0
  11. fastws_cli-0.0.35/tests/test_build.py +137 -0
  12. fastws_cli-0.0.35/tests/test_integrate.py +48 -0
  13. {fastws_cli-0.0.33 → fastws_cli-0.0.35}/tests/test_procs.py +11 -4
  14. {fastws_cli-0.0.33 → fastws_cli-0.0.35}/tests/test_sync.py +11 -8
  15. fastws_cli-0.0.33/fastws/cargo.py +0 -271
  16. fastws_cli-0.0.33/tests/test_build.py +0 -75
  17. {fastws_cli-0.0.33 → fastws_cli-0.0.35}/LICENSE +0 -0
  18. {fastws_cli-0.0.33 → fastws_cli-0.0.35}/fastws/build_backend.py +0 -0
  19. {fastws_cli-0.0.33 → fastws_cli-0.0.35}/fastws/news.py +0 -0
  20. {fastws_cli-0.0.33 → fastws_cli-0.0.35}/fastws/procs.py +0 -0
  21. {fastws_cli-0.0.33 → fastws_cli-0.0.35}/fastws/releases.py +0 -0
  22. {fastws_cli-0.0.33 → fastws_cli-0.0.35}/fastws/timelock.py +0 -0
  23. {fastws_cli-0.0.33 → fastws_cli-0.0.35}/fastws_cli.egg-info/dependency_links.txt +0 -0
  24. {fastws_cli-0.0.33 → fastws_cli-0.0.35}/fastws_cli.egg-info/entry_points.txt +0 -0
  25. {fastws_cli-0.0.33 → fastws_cli-0.0.35}/fastws_cli.egg-info/requires.txt +0 -0
  26. {fastws_cli-0.0.33 → fastws_cli-0.0.35}/fastws_cli.egg-info/top_level.txt +0 -0
  27. {fastws_cli-0.0.33 → fastws_cli-0.0.35}/pyproject.toml +0 -0
  28. {fastws_cli-0.0.33 → fastws_cli-0.0.35}/setup.cfg +0 -0
  29. {fastws_cli-0.0.33 → fastws_cli-0.0.35}/tests/test_build_backend.py +0 -0
  30. {fastws_cli-0.0.33 → fastws_cli-0.0.35}/tests/test_news.py +0 -0
  31. {fastws_cli-0.0.33 → fastws_cli-0.0.35}/tests/test_releases.py +0 -0
  32. {fastws_cli-0.0.33 → fastws_cli-0.0.35}/tests/test_setup.py +0 -0
  33. {fastws_cli-0.0.33 → fastws_cli-0.0.35}/tests/test_store.py +0 -0
  34. {fastws_cli-0.0.33 → fastws_cli-0.0.35}/tests/test_timelock.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fastws-cli
3
- Version: 0.0.33
3
+ Version: 0.0.35
4
4
  Summary: Fast workspace tools for multi-repo management
5
5
  Author: Jeremy Howard
6
6
  License-Expression: Apache-2.0
@@ -205,9 +205,10 @@ Build a mixed Rust/Python project's extension and opted-in binaries, keep them w
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
 
@@ -253,21 +254,54 @@ cargo wait --path /path/to/workspace
253
254
 
254
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.
255
256
 
256
- ### Rust tests from Python
257
+ ### Rust builds and tests from Python
257
258
 
258
- `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. Its `timeout` defaults to 30 seconds shared across the test binaries, excluding compilation. 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.
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. Outside `FASTWS_JOB`, a timed-out binary is reported as a failed suite with `timed_out` set. Inside a marked job, its timeout raises so the job owner can clean up descendants. 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.
259
260
 
260
- 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.
261
+ Where a root has `.config/nextest.toml`, `cargo nextest run` runs the built tests instead: each test in its own process, every binary at once, with the config's slow-test timeouts and groups. `profiles` lists the nextest profiles to run one after another, `('default',)` unless given. `timeout` bounds each profile's run, and each binary's results come from nextest's JUnit report, which lists no ignored tests. `--profile NAME` passes to nextest as `--cargo-profile NAME`, and `test_args` follow `--`.
262
+
263
+ `root` can also be a list of roots, which build one after another. Roots without nextest then run their binaries in turn. Each profile then runs at the same time in every root whose config defines it: tests in different roots share a phase, and no phase starts before the previous one ends. Suite names start with the root's directory name, and with several profiles also with the profile, as in `app timed: tests/speed.rs`. A failure ends the run after its profile unless `--no-fail-fast` is given.
264
+
265
+ 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.
261
266
 
262
267
  ```python
263
268
  from fastws.cargo import run_tests
264
269
 
265
270
  native = run_tests('.', '--no-fail-fast')
266
271
  focused = run_tests('.', '--test', 'core', test_args=('test_name', '--nocapture'))
272
+ phased = run_tests(['app', 'lib'], profiles=('untimed', 'timed'))
267
273
  ```
268
274
 
269
275
  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.
270
276
 
277
+ `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.
278
+
279
+ ```python
280
+ from fastws.cargo import check
281
+
282
+ lints = check('.', 'clippy', '--all-targets', '--', '-W', 'clippy::too_many_lines', env={'CLIPPY_CONF_DIR': 'review'})
283
+ errors = [d for d in lints.diagnostics if d.level == 'error']
284
+ sizes = check('.', 'llvm-lines', '--lib', diagnostics=False).text
285
+ ```
286
+
287
+ ### Integration snapshots
288
+
289
+ `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.
290
+
291
+ `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.
292
+
293
+ ```python
294
+ from fastws.cargo import run_tests
295
+ from fastws.integrate import by_owner, code_lines, errors, integrate, owners, record
296
+
297
+ root = '~/git/integration'
298
+ commits = integrate({'basedpl': '~/aai-ws/basedpl', 'rustymath': '~/aai-ws/rustymath'}, root)
299
+ errs = errors(root, ['rustymath', 'basedpl'])
300
+ groups = by_owner(errs, owners('~/aai-ws/basedpl/meta/owners.md'))
301
+ tests = {r: run_tests(f'{root}/{r}', '--no-fail-fast') for r in errs if not errs[r]}
302
+ sizes = {r: code_lines(f'{root}/{r}/src') for r in errs}
303
+ counts = record(f'{root}/run8.md', 'Run 8', commits, groups, tests=tests, sizes=sizes)
304
+ ```
271
305
  ### `ws-sync`
272
306
 
273
307
  Syncing proceeds in this order:
@@ -285,7 +319,7 @@ Fastws maintains `tool.uv.workspace.exclude`. It excludes unlisted top-level dir
285
319
 
286
320
  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.
287
321
 
288
- 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.
322
+ 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, that some local manifest requires from crates.io: by version alone, without `path`, `git` or `workspace`, in any dependency table or `[workspace.dependencies]`. 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.
289
323
 
290
324
  Sync also defaults `[term] quiet = true` in that file to suppress routine Cargo output and warnings. An explicit `quiet = false` is preserved.
291
325
 
@@ -293,6 +327,8 @@ Do not commit a `Cargo.lock` generated under these patches. Its source-less loca
293
327
 
294
328
  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`.
295
329
 
330
+ 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.
331
+
296
332
  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 `_`.
297
333
 
298
334
  Each sync updates `workspaces` in the root `package.json`. It creates the file when needed. It preserves external paths, globs, and unrelated settings. It reports added and removed packages.
@@ -177,9 +177,10 @@ Build a mixed Rust/Python project's extension and opted-in binaries, keep them w
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
 
@@ -225,21 +226,54 @@ cargo wait --path /path/to/workspace
225
226
 
226
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.
227
228
 
228
- ### Rust tests from Python
229
+ ### Rust builds and tests from Python
229
230
 
230
- `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. Its `timeout` defaults to 30 seconds shared across the test binaries, excluding compilation. 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.
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. Outside `FASTWS_JOB`, a timed-out binary is reported as a failed suite with `timed_out` set. Inside a marked job, its timeout raises so the job owner can clean up descendants. 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.
231
232
 
232
- 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.
233
+ Where a root has `.config/nextest.toml`, `cargo nextest run` runs the built tests instead: each test in its own process, every binary at once, with the config's slow-test timeouts and groups. `profiles` lists the nextest profiles to run one after another, `('default',)` unless given. `timeout` bounds each profile's run, and each binary's results come from nextest's JUnit report, which lists no ignored tests. `--profile NAME` passes to nextest as `--cargo-profile NAME`, and `test_args` follow `--`.
234
+
235
+ `root` can also be a list of roots, which build one after another. Roots without nextest then run their binaries in turn. Each profile then runs at the same time in every root whose config defines it: tests in different roots share a phase, and no phase starts before the previous one ends. Suite names start with the root's directory name, and with several profiles also with the profile, as in `app timed: tests/speed.rs`. A failure ends the run after its profile unless `--no-fail-fast` is given.
236
+
237
+ 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.
233
238
 
234
239
  ```python
235
240
  from fastws.cargo import run_tests
236
241
 
237
242
  native = run_tests('.', '--no-fail-fast')
238
243
  focused = run_tests('.', '--test', 'core', test_args=('test_name', '--nocapture'))
244
+ phased = run_tests(['app', 'lib'], profiles=('untimed', 'timed'))
239
245
  ```
240
246
 
241
247
  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.
242
248
 
249
+ `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.
250
+
251
+ ```python
252
+ from fastws.cargo import check
253
+
254
+ lints = check('.', 'clippy', '--all-targets', '--', '-W', 'clippy::too_many_lines', env={'CLIPPY_CONF_DIR': 'review'})
255
+ errors = [d for d in lints.diagnostics if d.level == 'error']
256
+ sizes = check('.', 'llvm-lines', '--lib', diagnostics=False).text
257
+ ```
258
+
259
+ ### Integration snapshots
260
+
261
+ `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.
262
+
263
+ `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.
264
+
265
+ ```python
266
+ from fastws.cargo import run_tests
267
+ from fastws.integrate import by_owner, code_lines, errors, integrate, owners, record
268
+
269
+ root = '~/git/integration'
270
+ commits = integrate({'basedpl': '~/aai-ws/basedpl', 'rustymath': '~/aai-ws/rustymath'}, root)
271
+ errs = errors(root, ['rustymath', 'basedpl'])
272
+ groups = by_owner(errs, owners('~/aai-ws/basedpl/meta/owners.md'))
273
+ tests = {r: run_tests(f'{root}/{r}', '--no-fail-fast') for r in errs if not errs[r]}
274
+ sizes = {r: code_lines(f'{root}/{r}/src') for r in errs}
275
+ counts = record(f'{root}/run8.md', 'Run 8', commits, groups, tests=tests, sizes=sizes)
276
+ ```
243
277
  ### `ws-sync`
244
278
 
245
279
  Syncing proceeds in this order:
@@ -257,7 +291,7 @@ Fastws maintains `tool.uv.workspace.exclude`. It excludes unlisted top-level dir
257
291
 
258
292
  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.
259
293
 
260
- 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.
294
+ 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, that some local manifest requires from crates.io: by version alone, without `path`, `git` or `workspace`, in any dependency table or `[workspace.dependencies]`. 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.
261
295
 
262
296
  Sync also defaults `[term] quiet = true` in that file to suppress routine Cargo output and warnings. An explicit `quiet = false` is preserved.
263
297
 
@@ -265,6 +299,8 @@ Do not commit a `Cargo.lock` generated under these patches. Its source-less loca
265
299
 
266
300
  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`.
267
301
 
302
+ 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.
303
+
268
304
  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 `_`.
269
305
 
270
306
  Each sync updates `workspaces` in the root `package.json`. It creates the file when needed. It preserves external paths, globs, and unrelated settings. It reports added and removed packages.
@@ -1,4 +1,6 @@
1
- __version__ = "0.0.33"
1
+ __version__ = "0.0.35"
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,377 @@
1
+ r"Build, test and install Cargo artifacts for mixed Rust/Python projects."
2
+ import json, os, re, shutil, signal, subprocess, sys, sysconfig, tempfile, time
3
+ from collections import Counter
4
+ from concurrent.futures import ThreadPoolExecutor
5
+ from dataclasses import dataclass, field
6
+ from pathlib import Path
7
+ from typing import NamedTuple
8
+ from xml.etree import ElementTree
9
+ from . import store
10
+ from .procs import bounded, JOB_ENV
11
+ from ._toml import tomllib
12
+
13
+
14
+ class ConfigError(ValueError):
15
+ "A project's `pyproject.toml` lacks a setting that a command needs."
16
+
17
+
18
+ def settings(root):
19
+ "Read the project's tool configuration."
20
+ return tomllib.loads((Path(root).expanduser()/'pyproject.toml').read_text()).get('tool', {})
21
+
22
+
23
+ def _dev_profile(root):
24
+ return settings(root).get('fastws', {}).get('dev-profile', 'test') if (root/'pyproject.toml').exists() else 'test'
25
+
26
+
27
+ def cargo(root, *args):
28
+ "Run Cargo in the project and return its standard output. A failure raises `CalledProcessError`, holding Cargo's standard error."
29
+ return subprocess.run(['cargo', *args], cwd=Path(root).expanduser(), capture_output=True, text=True, check=True).stdout
30
+
31
+
32
+ def members(root):
33
+ "The manifests of the workspace's member packages."
34
+ return {Path(p['manifest_path']) for p in json.loads(cargo(root, 'metadata', '--no-deps', '--format-version', '1'))['packages']}
35
+
36
+
37
+ _TEST = re.compile(r'^test (.+) \.\.\. (ok|FAILED|ignored)')
38
+ _FAILURE = re.compile(r'^---- (.+) std(?:out|err) ----$')
39
+
40
+
41
+ class Diagnostic(NamedTuple):
42
+ "A compiler message at its primary span, read from Cargo's JSON messages."
43
+ path:str; line:int; col:int; level:str; message:str
44
+ code:str|None=None # The error code or lint, such as `E0308` or `clippy::too_many_lines`
45
+ def __str__(self): return f'{self.level}{f"[{self.code}]" if self.code else ""}: {self.path}:{self.line}: {self.message}'
46
+
47
+
48
+ def _run(root, args, env=None, diagnostics=True):
49
+ "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."
50
+ flags = ('--message-format=json-diagnostic-short',) if diagnostics else ()
51
+ p = bounded(['cargo', args[0], *flags, *args[1:]], seconds=None, merge=True, cwd=root, env=os.environ | (env or {}))
52
+ if not diagnostics: return p.returncode, p.stdout, [], []
53
+ text, found, rest = [], [], []
54
+ for line in p.stdout.splitlines(keepends=True):
55
+ if not line.startswith('{'): text.append(line); continue
56
+ m = json.loads(line)
57
+ if m['reason'] != 'compiler-message': rest.append(m); continue
58
+ msg = m['message']
59
+ text.append(msg['rendered'])
60
+ if s := next((s for s in msg['spans'] if s['is_primary']), None):
61
+ found.append(Diagnostic(s['file_name'], s['line_start'], s['column_start'], msg['level'], msg['message'], (msg['code'] or {}).get('code')))
62
+ return p.returncode, ''.join(text), list(dict.fromkeys(found)), rest
63
+
64
+
65
+ def _lines(text, n=20, tail=False):
66
+ "Up to `n` lines of `text` (None: all), indented, from its end if `tail`, with a count of the lines left out."
67
+ lines = text.splitlines()
68
+ if n is None or len(lines) <= n: return [f' {l}' for l in lines]
69
+ shown = [f' {l}' for l in (lines[-n:] if tail else lines[:n])]
70
+ more = f' … {len(lines)-n} more lines'
71
+ return [more, *shown] if tail else [*shown, more]
72
+
73
+
74
+ @dataclass
75
+ class Suite:
76
+ "What one native test binary printed."
77
+ name:str # The target source, such as `tests/core.rs` or `unittests src/lib.rs`
78
+ output:str # Everything the binary printed, standard output and standard error merged
79
+ tests:dict # Each test's outcome: `ok`, `FAILED` or `ignored`
80
+ failures:dict # Each failing test's captured output
81
+ summary:str # libtest's `test result:` line, empty for a binary that crashed or has its own harness
82
+ failed:bool # Did the binary exit unsuccessfully?
83
+ timed_out:bool=False # Was the binary killed for running out of time?
84
+
85
+ @classmethod
86
+ def read(cls, name, lines, returncode, timed_out=False):
87
+ "Read one binary's lines and exit status, and whether it was killed for running out of time."
88
+ failures, current, in_failures = {}, None, False
89
+ for line in lines:
90
+ if line in ('successes:', 'failures:'): current, in_failures = None, line == 'failures:'
91
+ elif in_failures and (m := _FAILURE.match(line)): current = failures.setdefault(m[1], [])
92
+ elif current is not None: current.append(line)
93
+ return cls(name, '\n'.join(lines), {m[1]: m[2] for l in lines if (m := _TEST.match(l))},
94
+ {k: '\n'.join(v).strip('\n') for k,v in failures.items()},
95
+ next((l for l in lines if l.startswith('test result: ')), ''), bool(returncode), timed_out)
96
+
97
+ def __str__(self):
98
+ n = Counter(self.tests.values())
99
+ status = ', '.join(f'{n[o]} {w}' for o,w in [('ok', 'passed'), ('FAILED', 'failed'), ('ignored', 'ignored')] if n[o])
100
+ if self.failed and not n['FAILED']:
101
+ end = 'ran out of time' if self.timed_out else 'failed'
102
+ status = f'{status}, then {end}' if status else end
103
+ time = f' in {m[1]}' if (m := re.search(r'finished in (\S+)$', self.summary)) else ''
104
+ return f'{self.name}: {status or "ok"}{time}'
105
+
106
+
107
+ @dataclass
108
+ class Results:
109
+ "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."
110
+ command:str # The Cargo subcommand, such as `test` or `clippy`
111
+ returncode:int
112
+ text:str # The build's messages and each binary's merged standard output and standard error
113
+ diagnostics:list # Each `Diagnostic` from the build, before the first binary ran
114
+ suites:list=field(default_factory=list) # Each test binary's `Suite`, in execution order
115
+ seconds:float=0 # Wall time spent executing native test binaries, excluding compilation
116
+
117
+ @property
118
+ def ok(self): return self.returncode == 0
119
+
120
+ def summary(self, lines:int|None=20):
121
+ "The outcome, compiler messages, each binary's result and each failure's output, with up to `lines` lines of each output (None: all)"
122
+ res = [f'cargo {self.command} passed' if self.ok else f'cargo {self.command} failed with exit status {self.returncode}']
123
+ diags = sorted(self.diagnostics, key=lambda d: d.level != 'error')
124
+ res += [str(d) for d in diags[:lines]]
125
+ if lines is not None and len(diags) > lines: res.append(f'{len(diags)-lines} more compiler messages in `diagnostics`')
126
+ ran = [s for s in self.suites if s.tests or not s.summary]
127
+ for s in ran:
128
+ res.append(str(s))
129
+ for t,out in s.failures.items(): res += [f' FAILED {t}', *_lines(out, lines)]
130
+ if s.failed and not s.failures: res += _lines(s.output, lines, tail=True)
131
+ if idle := len(self.suites) - len(ran): res.append(f'{idle} test binaries ran no tests')
132
+ if not (self.ok or self.suites or diags): res += _lines(self.text, lines, tail=True)
133
+ return '\n'.join(res)
134
+ __repr__ = summary
135
+
136
+
137
+ def check(
138
+ root, # A Cargo package or workspace (expands `~`)
139
+ *args, # The Cargo subcommand and its options, such as `'clippy', '--all-targets', '--', '-W', 'clippy::pedantic'`; none: `check --all-targets`
140
+ env:dict=None, # Environment variables to set for the run, such as `{'CLIPPY_CONF_DIR': '/path/to/review'}`
141
+ diagnostics:bool=True, # Ask Cargo for JSON compiler messages? False runs a subcommand that rejects `--message-format`, such as `llvm-lines`, with no diagnostics
142
+ )->Results:
143
+ "Run a Cargo subcommand and return its `Results`, with each compiler message as a `Diagnostic`. A failing run is a result, not an exception."
144
+ args = args or ('check', '--all-targets')
145
+ return Results(args[0], *_run(Path(root).expanduser(), args, env, diagnostics)[:3])
146
+
147
+
148
+ def _bounded(cmd, timeout, **kwargs):
149
+ "`bounded(cmd, timeout, merge=True, **kwargs)`, and whether it ran out of time. Outside `FASTWS_JOB`, a timeout gives a killed process's result; inside a marked job, it raises so the job's owner can clean up descendants."
150
+ try: return bounded(cmd, timeout, merge=True, **kwargs), False
151
+ except TimeoutError as e:
152
+ if os.environ.get(JOB_ENV): raise
153
+ return subprocess.CompletedProcess(cmd, -signal.SIGKILL, f'{e}\n'), True
154
+
155
+
156
+ def _suite_name(m):
157
+ "The `Suite` name of the test binary that Cargo artifact `m` describes: its target's source, prefixed `unittests ` unless it's an integration test."
158
+ name = Path(m['target']['src_path']).relative_to(Path(m['manifest_path']).parent).as_posix()
159
+ return name if 'test' in m['target']['kind'] else 'unittests ' + name
160
+
161
+
162
+ def _binary_id(m, packages):
163
+ "nextest's id for the test binary that Cargo artifact `m` describes, such as `app`, `app::more` or `app::bin/app`. `packages` maps package ids to names."
164
+ pkg, kind, name = packages[m['package_id']], m['target']['kind'][0], m['target']['name']
165
+ if kind == 'test': return f'{pkg}::{name}'
166
+ return f'{pkg}::{kind}/{name}' if kind in ('bin', 'example', 'bench') else pkg
167
+
168
+
169
+ def _junit(path, names):
170
+ "Each binary's `Suite` from nextest's JUnit report at `path`. `names` maps nextest's binary ids to suite names. Ignored tests don't appear."
171
+ suites = []
172
+ for ts in ElementTree.parse(path).getroot().iter('testsuite'):
173
+ tests, failures, late = {}, {}, False
174
+ for tc in ts.iter('testcase'):
175
+ t, bad = tc.get('name'), next(iter(tc.findall('failure') + tc.findall('error')), None)
176
+ tests[t] = 'ok' if bad is None else 'FAILED'
177
+ if bad is None: continue
178
+ failures[t] = '\n'.join(s for s in (tc.findtext('system-out', '').strip('\n'), tc.findtext('system-err', '').strip('\n') or bad.text or '') if s)
179
+ late |= 'timeout' in bad.get('type', '')
180
+ suites.append(Suite(names.get(ts.get('name'), ts.get('name')), '\n\n'.join(failures.values()), tests, failures, '', bool(failures), late))
181
+ return suites
182
+
183
+
184
+ def _nextest(root, args, test_args, env, timeout, artifacts, profile='default'):
185
+ "Run the built tests with `cargo nextest run --profile profile` under `timeout` as a whole: its exit status, its output, each binary's `Suite` and the run's seconds. Cargo's `--profile` in `args` becomes nextest's `--cargo-profile`."
186
+ packages = {p['id']: p['name'] for p in json.loads(cargo(root, 'metadata', '--format-version', '1', '--no-deps'))['packages']}
187
+ names = {_binary_id(m, packages): _suite_name(m) for m in artifacts}
188
+ args = [re.sub(r'^--profile(?==|$)', '--cargo-profile', a) for a in map(str, args)]
189
+ with tempfile.TemporaryDirectory() as d:
190
+ report, tool = Path(d)/'junit.xml', Path(d)/'fastws.toml'
191
+ tool.write_text(f'[profile.{profile}.junit]\npath = {json.dumps(str(report))}\n')
192
+ cmd = ['cargo', 'nextest', 'run', '--tool-config-file', f'fastws:{tool}', '--profile', profile, '--show-progress=none', '--color=never', *args, *(('--', *test_args) if test_args else ())]
193
+ start = time.monotonic()
194
+ p, late = _bounded(cmd, timeout, cwd=root, env=os.environ | env)
195
+ suites = _junit(report, names) if report.exists() else [Suite('nextest', p.stdout, {}, {}, '', True, late)]
196
+ return p.returncode, p.stdout, suites, time.monotonic() - start
197
+
198
+
199
+ def _libtest(root, args, test_args, env, timeout, artifacts):
200
+ "Run each built test binary in turn from its package directory under `timeout`: the first failing exit status, the output and each binary's `Suite`. Without `--no-fail-fast` in `args`, the first failing binary ends the run."
201
+ code, text, suites = 0, '', []
202
+ for m in artifacts:
203
+ package, name = Path(m['manifest_path']).parent, _suite_name(m)
204
+ p, late = _bounded([m['executable'], *test_args], timeout, cwd=package, env=os.environ | env | {'CARGO_MANIFEST_DIR': str(package)})
205
+ suites.append(Suite.read(name, p.stdout.splitlines(), p.returncode, late))
206
+ text += f' Running {name} ({m["executable"]})\n' + p.stdout
207
+ if p.returncode:
208
+ code = code or p.returncode
209
+ if '--no-fail-fast' not in args: break
210
+ return code, text, suites
211
+
212
+
213
+ def _nextest_profiles(root):
214
+ "The nextest profiles that `root`'s `.config/nextest.toml` defines, `default` included, or None without that file."
215
+ config = root/'.config/nextest.toml'
216
+ return {'default', *tomllib.loads(config.read_text()).get('profile', {})} if config.exists() else None
217
+
218
+
219
+ def run_tests(
220
+ root, # A Cargo package or workspace (expands `~`), or a list of them
221
+ *args, # Cargo build/selection options, such as `'--test', 'core'` or `'--profile', 'checked'`; `--no-fail-fast` runs every binary
222
+ test_args=(), # Arguments passed directly to each test binary, such as `('name', '--nocapture')`; nextest gets them after `--`
223
+ env:dict=None, # Environment variables to set for the run, such as `{'BASEDPL_COSTS': '1'}`
224
+ timeout:float=30, # Seconds that each native test binary, or each nextest run of one profile in one root, may take after building; None: no limit
225
+ profiles:tuple=('default',) # The nextest profiles to run in turn, each at once in every root whose `.config/nextest.toml` defines it
226
+ )->Results:
227
+ "Build native tests once per root, with no time limit, then run Cargo's reported executables directly from their package directories. Where a root has `.config/nextest.toml`, `cargo nextest run` runs them instead, each test in its own process, once for each of `profiles` that its config defines, and each binary's results come from nextest's JUnit report. Several roots build in turn. Roots without nextest then run in turn, and each profile then runs in every root that defines it at once. Without `--no-fail-fast`, a failure ends the run after its binary, or after its profile. With several roots, each suite's name starts with its root's directory name, and with several profiles, with the profile. Source edits after building cannot trigger another build; results describe the compiled build, not the latest working tree. `timeout` bounds each binary, or each root's run of a profile, excluding compilation. Outside `FASTWS_JOB`, a run that runs out of time is reported as a failed suite with `timed_out` set. Inside a marked job, the timeout raises so its owner can clean up descendants. An omitted Cargo profile uses each root's 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."
228
+ roots = [Path(r).expanduser().resolve() for r in (root if isinstance(root, (list, tuple)) else [root])]
229
+ env = {'CARGO_TERM_QUIET': 'false', 'CARGO_TERM_COLOR': 'never'} | (env or {})
230
+ has_profile = any(a in ('-r', '--release', '--profile') or isinstance(a, str) and a.startswith('--profile=') for a in args)
231
+ text, found, built = '', [], []
232
+ for r in roots:
233
+ rargs = args if has_profile else (*args, '--profile', _dev_profile(r))
234
+ code, out, diags, rest = _run(r, ('test', '--no-run', *rargs), env)
235
+ text, found = text + out, found + diags
236
+ if code: return Results('test', code, text, found)
237
+ artifacts = [m for m in rest if m['reason'] == 'compiler-artifact' and m['profile']['test'] and m.get('executable')]
238
+ built.append((r, rargs, artifacts, _nextest_profiles(r)))
239
+ def named(r, p, suites):
240
+ "`suites`, freshly read, renamed for root `r` and profile `p` where several roots or profiles ran"
241
+ t = ' '.join(x for x in (r.name if len(roots) > 1 else '', p if p and len(profiles) > 1 else '') if x)
242
+ for s in suites: s.name = f'{t}: {s.name}' if t else s.name
243
+ return t, suites
244
+ fail_fast, code, suites, start = '--no-fail-fast' not in args, 0, [], time.monotonic()
245
+ for r, rargs, artifacts, defined in built:
246
+ if defined is not None: continue
247
+ c, out, ss = _libtest(r, rargs, test_args, env, timeout, artifacts)
248
+ text, suites, code = text + out, suites + named(r, None, ss)[1], code or c
249
+ if code and fail_fast: return Results('test', code, text, found, suites, time.monotonic() - start)
250
+ for p in profiles:
251
+ jobs = [(r, rargs, artifacts) for r, rargs, artifacts, defined in built if p in (defined or ())]
252
+ with ThreadPoolExecutor(max(len(jobs), 1)) as ex: runs = list(ex.map(lambda j: _nextest(j[0], j[1], test_args, env, timeout, j[2], p), jobs))
253
+ for (r, _, _), (c, out, ss, _) in zip(jobs, runs):
254
+ t, ss = named(r, p, ss)
255
+ text, suites, code = text + (f' Nextest profile {p} in {r}\n' if t else '') + out, suites + ss, code or c
256
+ if code and fail_fast: break
257
+ return Results('test', code, text, found, suites, time.monotonic() - start)
258
+
259
+
260
+ def sources(root):
261
+ "The manifests, and the target directories or files, of the local packages that the workspace builds: its members and its path dependencies."
262
+ res = set()
263
+ for p in json.loads(cargo(root, 'metadata', '--format-version', '1'))['packages']:
264
+ if p['source'] is not None: continue
265
+ manifest = Path(p['manifest_path'])
266
+ res.add(manifest)
267
+ for t in p['targets']:
268
+ src = Path(t['src_path'])
269
+ res.add(src if src.parent == manifest.parent else src.parent)
270
+ return res
271
+
272
+
273
+ def _maturin_flags(target, env):
274
+ "Maturin's macOS linker flags without replacing existing compiler flags."
275
+ target = target or env.get('CARGO_BUILD_TARGET')
276
+ if not (target.endswith('-apple-darwin') if target else sys.platform == 'darwin'): return [], None
277
+ link_args = ['link-arg=-undefined', 'link-arg=dynamic_lookup']
278
+ if 'CARGO_ENCODED_RUSTFLAGS' in env:
279
+ flags = env['CARGO_ENCODED_RUSTFLAGS'].split('\x1f') if env['CARGO_ENCODED_RUSTFLAGS'] else []
280
+ elif 'RUSTFLAGS' in env: flags = env['RUSTFLAGS'].split()
281
+ else:
282
+ flags = [arg for flag in link_args for arg in ('-C', flag)]
283
+ return ['--config', 'build.rustflags=' + json.dumps(flags)], None
284
+ for flag in link_args:
285
+ if flag not in flags and '-C'+flag not in flags: flags += ['-C', flag]
286
+ return [], dict(env, CARGO_ENCODED_RUSTFLAGS='\x1f'.join(flags))
287
+
288
+
289
+ def build(
290
+ root, # The project directory, whose `tool.maturin` gives the defaults (expands `~`)
291
+ *,
292
+ library=True, # Build the library target?
293
+ binaries=False, # Build the binary targets?
294
+ profile=None, # Cargo profile; None: `tool.maturin.profile`, or Cargo's default
295
+ target=None, # Target triple; None: the host
296
+ features=None, # Features to enable, as a list or a string; None: `tool.maturin.features`
297
+ all_features=False, # Enable every feature? `tool.maturin`'s `all-features` also turns this on
298
+ no_default_features=False, # Leave out the default features? `tool.maturin`'s `no-default-features` also turns this on
299
+ for_maturin=False, # Use the macOS linker flags that maturin's own wheel build uses?
300
+ ):
301
+ "Build with Cargo and return the compiler artifacts of the workspace's packages."
302
+ root = Path(root).expanduser()
303
+ cfg = settings(root)['maturin']
304
+ cmd = ['build', '--manifest-path', str((root/'Cargo.toml').resolve()), '--message-format=json-diagnostic-short']
305
+ if library: cmd.append('--lib')
306
+ if binaries: cmd.append('--bins')
307
+ if profile := profile or cfg.get('profile'): cmd += ['--profile', profile]
308
+ if target: cmd += ['--target', target]
309
+ if features := features or cfg.get('features'):
310
+ cmd += ['--features', features if isinstance(features, str) else ','.join(features)]
311
+ for flag, active in [('all-features', all_features), ('no-default-features', no_default_features)]:
312
+ if active or cfg.get(flag): cmd.append('--' + flag)
313
+ extra, env = _maturin_flags(target, os.environ) if for_maturin else ([], None)
314
+ result = subprocess.run(['cargo', *cmd, *extra], cwd=root, stdout=subprocess.PIPE, text=True, env=env)
315
+ messages = [json.loads(line) for line in result.stdout.splitlines()]
316
+ for m in messages:
317
+ if m.get('reason') != 'compiler-message': continue
318
+ name = tomllib.loads(Path(m['manifest_path']).read_text())['package']['name']
319
+ for line in m['message']['rendered'].splitlines(): print(f'[{name}] {line}', file=sys.stderr)
320
+ result.check_returncode()
321
+ workspace = members(root)
322
+ return [m for m in messages if m.get('reason') == 'compiler-artifact' and Path(m['manifest_path']) in workspace]
323
+
324
+
325
+ def replace(source, destination):
326
+ "Replace an installed artifact without changing the file held by running processes."
327
+ destination.parent.mkdir(parents=True, exist_ok=True)
328
+ tmp = destination.with_name(f'{destination.name}.{os.getpid()}.tmp')
329
+ shutil.copy2(source, tmp)
330
+ os.replace(tmp, destination)
331
+ return destination
332
+
333
+
334
+ def develop(root, *, profile=None):
335
+ "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."
336
+ root = Path(root).expanduser()
337
+ with store.building(root) as started:
338
+ tool = settings(root)
339
+ cfg = tool['maturin']
340
+ if 'python-source' not in cfg: raise ConfigError(r'cargo develop requires tool.maturin.python-source')
341
+ binaries = tool.get('fastws', {}).get('native-binaries', False)
342
+ artifacts = build(root, binaries=binaries, profile=profile or _dev_profile(root))
343
+ artifact, = [m for m in artifacts if 'cdylib' in m['target']['crate_types']]
344
+ lib, = [Path(f) for f in artifact['filenames'] if Path(f).suffix in ('.dylib', '.so', '.dll')]
345
+ module = cfg.get('module-name', artifact['target']['name'])
346
+ src = root/cfg['python-source']
347
+ ext = src/Path(*module.split('.'))
348
+ ext = ext.with_name(ext.name + sysconfig.get_config_var('EXT_SUFFIX'))
349
+ env = Path(os.environ.get('VIRTUAL_ENV', sys.prefix))
350
+ scripts = env/('Scripts' if os.name == 'nt' else 'bin')
351
+ exes = [Path(m['executable']) for m in artifacts if binaries and m.get('executable')]
352
+ package = src/module.split('.')[0]
353
+ 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}
354
+ files |= {f'python/{ext.relative_to(src)}': lib} | {f'bin/{e.name}': e for e in exes}
355
+ with store.access():
356
+ kept = store.keep(root, files, dict.fromkeys(f for a in [lib, *exes] for f in store.dep_files(a)), started)
357
+ store.install(root, kept, {ext: f'python/{ext.relative_to(src)}'} | {scripts/e.name: f'bin/{e.name}' for e in exes})
358
+ store.collect()
359
+ if generator := tool.get('fastws', {}).get('post-develop-module'):
360
+ subprocess.run([sys.executable, '-m', generator], cwd=root, check=True)
361
+ # Editable builds through `build_backend` don't install the wheel's data files, which a wheel install puts under the prefix.
362
+ if 'data' in cfg:
363
+ data = root/cfg['data']/'data'
364
+ for f in data.rglob('*'):
365
+ if f.is_file(): replace(f, env/f.relative_to(data))
366
+
367
+
368
+ def stage_binaries(root, profile=None, target=None, features=None, all_features=False, no_default_features=False):
369
+ "Stage Python-free Cargo binaries for a maturin wheel build."
370
+ root = Path(root).expanduser()
371
+ cfg = settings(root)['maturin']
372
+ if 'data' not in cfg: raise ConfigError(r'staging native binaries requires tool.maturin.data')
373
+ dest = root/cfg['data']/'scripts'
374
+ artifacts = build(root, library=False, binaries=True, profile=profile, target=target, features=features,
375
+ all_features=all_features, no_default_features=no_default_features, for_maturin=True)
376
+ if dest.exists(): shutil.rmtree(dest)
377
+ return [replace(m['executable'], dest/Path(m['executable']).name) for m in artifacts if m.get('executable')]