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