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.
- {fastws_cli-0.0.33 → fastws_cli-0.0.35}/PKG-INFO +42 -6
- {fastws_cli-0.0.33 → fastws_cli-0.0.35}/README.md +41 -5
- {fastws_cli-0.0.33 → fastws_cli-0.0.35}/fastws/__init__.py +3 -1
- fastws_cli-0.0.35/fastws/_toml.py +4 -0
- fastws_cli-0.0.35/fastws/cargo.py +377 -0
- {fastws_cli-0.0.33 → fastws_cli-0.0.35}/fastws/core.py +54 -28
- fastws_cli-0.0.35/fastws/integrate.py +136 -0
- {fastws_cli-0.0.33 → fastws_cli-0.0.35}/fastws/store.py +6 -2
- {fastws_cli-0.0.33 → fastws_cli-0.0.35}/fastws_cli.egg-info/PKG-INFO +42 -6
- {fastws_cli-0.0.33 → fastws_cli-0.0.35}/fastws_cli.egg-info/SOURCES.txt +3 -0
- fastws_cli-0.0.35/tests/test_build.py +137 -0
- fastws_cli-0.0.35/tests/test_integrate.py +48 -0
- {fastws_cli-0.0.33 → fastws_cli-0.0.35}/tests/test_procs.py +11 -4
- {fastws_cli-0.0.33 → fastws_cli-0.0.35}/tests/test_sync.py +11 -8
- fastws_cli-0.0.33/fastws/cargo.py +0 -271
- fastws_cli-0.0.33/tests/test_build.py +0 -75
- {fastws_cli-0.0.33 → fastws_cli-0.0.35}/LICENSE +0 -0
- {fastws_cli-0.0.33 → fastws_cli-0.0.35}/fastws/build_backend.py +0 -0
- {fastws_cli-0.0.33 → fastws_cli-0.0.35}/fastws/news.py +0 -0
- {fastws_cli-0.0.33 → fastws_cli-0.0.35}/fastws/procs.py +0 -0
- {fastws_cli-0.0.33 → fastws_cli-0.0.35}/fastws/releases.py +0 -0
- {fastws_cli-0.0.33 → fastws_cli-0.0.35}/fastws/timelock.py +0 -0
- {fastws_cli-0.0.33 → fastws_cli-0.0.35}/fastws_cli.egg-info/dependency_links.txt +0 -0
- {fastws_cli-0.0.33 → fastws_cli-0.0.35}/fastws_cli.egg-info/entry_points.txt +0 -0
- {fastws_cli-0.0.33 → fastws_cli-0.0.35}/fastws_cli.egg-info/requires.txt +0 -0
- {fastws_cli-0.0.33 → fastws_cli-0.0.35}/fastws_cli.egg-info/top_level.txt +0 -0
- {fastws_cli-0.0.33 → fastws_cli-0.0.35}/pyproject.toml +0 -0
- {fastws_cli-0.0.33 → fastws_cli-0.0.35}/setup.cfg +0 -0
- {fastws_cli-0.0.33 → fastws_cli-0.0.35}/tests/test_build_backend.py +0 -0
- {fastws_cli-0.0.33 → fastws_cli-0.0.35}/tests/test_news.py +0 -0
- {fastws_cli-0.0.33 → fastws_cli-0.0.35}/tests/test_releases.py +0 -0
- {fastws_cli-0.0.33 → fastws_cli-0.0.35}/tests/test_setup.py +0 -0
- {fastws_cli-0.0.33 → fastws_cli-0.0.35}/tests/test_store.py +0 -0
- {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.
|
|
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]`.
|
|
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.
|
|
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
|
-
|
|
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,
|
|
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]`.
|
|
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.
|
|
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
|
-
|
|
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,
|
|
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.
|
|
@@ -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')]
|