fastws-cli 0.0.18__tar.gz → 0.0.20__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.
@@ -1,16 +1,16 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fastws-cli
3
- Version: 0.0.18
3
+ Version: 0.0.20
4
4
  Summary: Fast workspace tools for multi-repo management
5
5
  Author: Jeremy Howard
6
- License: Apache-2.0
6
+ License-Expression: Apache-2.0
7
7
  Project-URL: Homepage, https://github.com/AnswerDotAI/fastws
8
8
  Classifier: Programming Language :: Python :: 3
9
9
  Classifier: Programming Language :: Python :: 3 :: Only
10
10
  Requires-Python: >=3.10
11
11
  Description-Content-Type: text/markdown
12
12
  License-File: LICENSE
13
- Requires-Dist: fastcore>=2.2.28
13
+ Requires-Dist: fastcore>=2.2.33
14
14
  Requires-Dist: fastgit>=0.1.2
15
15
  Requires-Dist: ghapi>=2.0.6
16
16
  Requires-Dist: packaging
@@ -195,6 +195,34 @@ ws-build --project solveit # only this package and its transitive workspace dep
195
195
 
196
196
  `--project` selects by package name and follows declared runtime and build dependencies, including workspace packages in external checkouts. Other packages in the output directory are left untouched. Without it, the whole workspace is built as before.
197
197
 
198
+ ### `cargo develop`
199
+
200
+ Rebuild a mixed Rust/Python project's extension and replace it in the editable source package:
201
+
202
+ ```bash
203
+ cargo develop
204
+ cargo develop --path /path/to/project
205
+ ```
206
+
207
+ Run from the project root, or give its directory with `--path`. The command reads `python-source`, `module-name`, `manifest-path` and features from `[tool.maturin]`. It always builds with Cargo's `test` profile, matching bare `cargo test`. The project must produce a `cdylib` and set `python-source`. To use release settings without command-line flags, add `[profile.test]` with `inherits = "release"` to `Cargo.toml`. Keep incremental compilation enabled for the local crate in that profile.
208
+
209
+ 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.
210
+
211
+ Cargo must already build a loadable Python extension. For PyO3 extension-module builds on macOS, configure the linker in `build.rs` with `pyo3_build_config::add_extension_module_link_args()`. This command does not inject maturin's extra compiler flags.
212
+
213
+ To share the library build with `cargo test`, also produce an `rlib` and use the same features for both commands. Rust unit tests compile a separate library with `cfg(test)`; integration tests reuse the ordinary library. `cargo develop` does not change the project's Cargo configuration or its test layout.
214
+
215
+ Set `native-binaries = true` under `[tool.fastws]` to build and install the package's Cargo binary targets into the active venv alongside its extension. Cargo reports the executable paths; there is no separate list of binary names.
216
+
217
+ For wheels containing these binaries, use `fastws.build_backend` as the build backend and include `fastws-cli>=0.0.20` alongside maturin in `[build-system].requires`. Set `[tool.maturin].data` to the wheel-data directory. The backend stages Python-free binaries in its `scripts/` subdirectory, then delegates packaging to maturin. Explicit profile and target arguments from maturin's build settings also apply to the binaries.
218
+
219
+ Direct maturin CLI builds bypass the Python build backend. Stage the binaries first with the shared command:
220
+
221
+ ```bash
222
+ cargo stage --profile dist
223
+ maturin build --profile dist
224
+ ```
225
+
198
226
  ### `ws-sync`
199
227
 
200
228
  Syncing proceeds in this order:
@@ -208,7 +236,7 @@ By default, `ws-sync` uses the active venv's parent as the workspace root. It cr
208
236
 
209
237
  Project scanning respects `tool.uv.workspace.members` and `exclude`. If a member directory lacks `pyproject.toml`, such as a fresh empty clone, sync warns and skips the `uv sync` step.
210
238
 
211
- Fastws maintains `tool.uv.workspace.exclude`. It excludes unlisted top-level directories without a valid Python project. It also excludes listed Rust or JavaScript projects without `pyproject.toml`. For a listed checkout with none of `pyproject.toml`, `Cargo.toml`, or `package.json`, sync warns and skips installation.
239
+ Fastws maintains `tool.uv.workspace.exclude`. It excludes unlisted top-level directories without a valid Python project. It also excludes listed Rust or JavaScript projects without `pyproject.toml`. It also excludes listed project templates, whose `[project].name` still holds a `{placeholder}`, because uv cannot parse that name. For a listed checkout with none of `pyproject.toml`, `Cargo.toml`, or `package.json`, sync warns and skips installation.
212
240
 
213
241
  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.
214
242
 
@@ -224,7 +252,9 @@ Each sync updates `workspaces` in the root `package.json`. It creates the file w
224
252
 
225
253
  An optional tracked `package.json.shared` is merged into the root `package.json` on every sync. Object-valued fields merge one level deep, shared values win conflicts, and arrays replace rather than concatenate. Unrelated local settings remain; removing a shared key does not delete its local value. Workspace discovery still manages `workspaces`. Use this file for shared policy such as version-pinned npm `allowScripts` approvals.
226
254
 
227
- After `uv sync`, fastws runs `<tool> install` at the workspace root. It then runs `<tool> run build` in each JavaScript package containing both `Cargo.toml` and a `build` script. Cargo handles incremental compilation. Other build steps still run.
255
+ After `uv sync`, fastws runs `cargo develop` in each mixed Rust/Python project. Cargo skips unchanged compilation inputs. These projects should keep only packaging inputs in `tool.uv.cache-keys`; Rust source changes are handled by Cargo rather than triggering a separate maturin editable build. Maturin packages with `bindings = "bin"` keep their existing uv installation path.
256
+
257
+ Fastws then runs `<tool> install` at the workspace root. It runs `<tool> run build` in each JavaScript package containing both `Cargo.toml` and a `build` script. Cargo handles incremental compilation. Other build steps still run.
228
258
 
229
259
  For npm, sync hides routine install summaries and build-command banners. Install warnings, audit findings, unreviewed install scripts, and failure diagnostics remain visible. Build scripts retain their own output; configure their tools to suppress routine progress. Other package managers retain their normal output.
230
260
 
@@ -311,4 +341,4 @@ await check_release('mdhtml') # one repo: list of unreleased commit summarie
311
341
 
312
342
  ## Tests
313
343
 
314
- Run `pytest -q` for the regular suite. `pytest -q tests/test_setup.py -m slow` also builds the current package and bootstraps a fresh environment with real Git and uv; it may fetch package dependencies. Run `chkstyle fastws tests` after edits.
344
+ Run `pytest -q`. Keep tests concise and focused on substantive logic. Do not test policy choices, settings, constants, or orchestration. Do not add tests merely because code changed.
@@ -170,6 +170,34 @@ ws-build --project solveit # only this package and its transitive workspace dep
170
170
 
171
171
  `--project` selects by package name and follows declared runtime and build dependencies, including workspace packages in external checkouts. Other packages in the output directory are left untouched. Without it, the whole workspace is built as before.
172
172
 
173
+ ### `cargo develop`
174
+
175
+ Rebuild a mixed Rust/Python project's extension and replace it in the editable source package:
176
+
177
+ ```bash
178
+ cargo develop
179
+ cargo develop --path /path/to/project
180
+ ```
181
+
182
+ Run from the project root, or give its directory with `--path`. The command reads `python-source`, `module-name`, `manifest-path` and features from `[tool.maturin]`. It always builds with Cargo's `test` profile, matching bare `cargo test`. The project must produce a `cdylib` and set `python-source`. To use release settings without command-line flags, add `[profile.test]` with `inherits = "release"` to `Cargo.toml`. Keep incremental compilation enabled for the local crate in that profile.
183
+
184
+ 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
+ Cargo must already build a loadable Python extension. For PyO3 extension-module builds on macOS, configure the linker in `build.rs` with `pyo3_build_config::add_extension_module_link_args()`. This command does not inject maturin's extra compiler flags.
187
+
188
+ To share the library build with `cargo test`, also produce an `rlib` and use the same features for both commands. Rust unit tests compile a separate library with `cfg(test)`; integration tests reuse the ordinary library. `cargo develop` does not change the project's Cargo configuration or its test layout.
189
+
190
+ Set `native-binaries = true` under `[tool.fastws]` to build and install the package's Cargo binary targets into the active venv alongside its extension. Cargo reports the executable paths; there is no separate list of binary names.
191
+
192
+ For wheels containing these binaries, use `fastws.build_backend` as the build backend and include `fastws-cli>=0.0.20` alongside maturin in `[build-system].requires`. Set `[tool.maturin].data` to the wheel-data directory. The backend stages Python-free binaries in its `scripts/` subdirectory, then delegates packaging to maturin. Explicit profile and target arguments from maturin's build settings also apply to the binaries.
193
+
194
+ Direct maturin CLI builds bypass the Python build backend. Stage the binaries first with the shared command:
195
+
196
+ ```bash
197
+ cargo stage --profile dist
198
+ maturin build --profile dist
199
+ ```
200
+
173
201
  ### `ws-sync`
174
202
 
175
203
  Syncing proceeds in this order:
@@ -183,7 +211,7 @@ By default, `ws-sync` uses the active venv's parent as the workspace root. It cr
183
211
 
184
212
  Project scanning respects `tool.uv.workspace.members` and `exclude`. If a member directory lacks `pyproject.toml`, such as a fresh empty clone, sync warns and skips the `uv sync` step.
185
213
 
186
- Fastws maintains `tool.uv.workspace.exclude`. It excludes unlisted top-level directories without a valid Python project. It also excludes listed Rust or JavaScript projects without `pyproject.toml`. For a listed checkout with none of `pyproject.toml`, `Cargo.toml`, or `package.json`, sync warns and skips installation.
214
+ Fastws maintains `tool.uv.workspace.exclude`. It excludes unlisted top-level directories without a valid Python project. It also excludes listed Rust or JavaScript projects without `pyproject.toml`. It also excludes listed project templates, whose `[project].name` still holds a `{placeholder}`, because uv cannot parse that name. For a listed checkout with none of `pyproject.toml`, `Cargo.toml`, or `package.json`, sync warns and skips installation.
187
215
 
188
216
  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.
189
217
 
@@ -199,7 +227,9 @@ Each sync updates `workspaces` in the root `package.json`. It creates the file w
199
227
 
200
228
  An optional tracked `package.json.shared` is merged into the root `package.json` on every sync. Object-valued fields merge one level deep, shared values win conflicts, and arrays replace rather than concatenate. Unrelated local settings remain; removing a shared key does not delete its local value. Workspace discovery still manages `workspaces`. Use this file for shared policy such as version-pinned npm `allowScripts` approvals.
201
229
 
202
- After `uv sync`, fastws runs `<tool> install` at the workspace root. It then runs `<tool> run build` in each JavaScript package containing both `Cargo.toml` and a `build` script. Cargo handles incremental compilation. Other build steps still run.
230
+ After `uv sync`, fastws runs `cargo develop` in each mixed Rust/Python project. Cargo skips unchanged compilation inputs. These projects should keep only packaging inputs in `tool.uv.cache-keys`; Rust source changes are handled by Cargo rather than triggering a separate maturin editable build. Maturin packages with `bindings = "bin"` keep their existing uv installation path.
231
+
232
+ Fastws then runs `<tool> install` at the workspace root. It runs `<tool> run build` in each JavaScript package containing both `Cargo.toml` and a `build` script. Cargo handles incremental compilation. Other build steps still run.
203
233
 
204
234
  For npm, sync hides routine install summaries and build-command banners. Install warnings, audit findings, unreviewed install scripts, and failure diagnostics remain visible. Build scripts retain their own output; configure their tools to suppress routine progress. Other package managers retain their normal output.
205
235
 
@@ -286,4 +316,4 @@ await check_release('mdhtml') # one repo: list of unreleased commit summarie
286
316
 
287
317
  ## Tests
288
318
 
289
- Run `pytest -q` for the regular suite. `pytest -q tests/test_setup.py -m slow` also builds the current package and bootstraps a fresh environment with real Git and uv; it may fetch package dependencies. Run `chkstyle fastws tests` after edits.
319
+ Run `pytest -q`. Keep tests concise and focused on substantive logic. Do not test policy choices, settings, constants, or orchestration. Do not add tests merely because code changed.
@@ -1,4 +1,6 @@
1
- __version__ = "0.0.18"
1
+ __version__ = "0.0.20"
2
+
3
+
2
4
 
3
5
 
4
6
 
@@ -0,0 +1,38 @@
1
+ r"Maturin build backend that includes Cargo binaries in Python wheels."
2
+ import argparse
3
+ from pathlib import Path
4
+ import maturin
5
+ from .cargo import settings, stage_binaries
6
+
7
+
8
+ def _stage(config_settings):
9
+ parser = argparse.ArgumentParser(add_help=False)
10
+ parser.add_argument('--profile')
11
+ parser.add_argument('--release', action='store_true')
12
+ parser.add_argument('--target')
13
+ args, _ = parser.parse_known_args(maturin.get_maturin_pep517_args(config_settings))
14
+ root = Path.cwd()
15
+ profile = args.profile or ('release' if args.release else settings(root)['maturin'].get('profile', 'release'))
16
+ stage_binaries(root, profile, args.target)
17
+
18
+
19
+ def build_wheel(wheel_directory, config_settings=None, metadata_directory=None):
20
+ _stage(config_settings)
21
+ return maturin.build_wheel(wheel_directory, config_settings, metadata_directory)
22
+
23
+
24
+ def build_editable(wheel_directory, config_settings=None, metadata_directory=None):
25
+ _stage(config_settings)
26
+ return maturin.build_editable(wheel_directory, config_settings, metadata_directory)
27
+
28
+
29
+ def prepare_metadata_for_build_wheel(metadata_directory, config_settings=None):
30
+ (Path.cwd()/settings(Path.cwd())['maturin']['data']).mkdir(parents=True, exist_ok=True)
31
+ return maturin.prepare_metadata_for_build_wheel(metadata_directory, config_settings)
32
+
33
+
34
+ prepare_metadata_for_build_editable = prepare_metadata_for_build_wheel
35
+ build_sdist = maturin.build_sdist
36
+ get_requires_for_build_wheel = maturin.get_requires_for_build_wheel
37
+ get_requires_for_build_editable = maturin.get_requires_for_build_editable
38
+ get_requires_for_build_sdist = maturin.get_requires_for_build_sdist
@@ -0,0 +1,67 @@
1
+ r"Build and install Cargo artifacts for mixed Rust/Python projects."
2
+ import json, os, shutil, subprocess, sys, sysconfig
3
+ from pathlib import Path
4
+
5
+ try: import tomllib
6
+ except ModuleNotFoundError: import tomli as tomllib
7
+
8
+
9
+ def settings(root):
10
+ "Read the project's tool configuration."
11
+ return tomllib.loads((root/'pyproject.toml').read_text()).get('tool', {})
12
+
13
+
14
+ def build(root, *, library=True, binaries=False, profile=None, target=None, no_default_features=False):
15
+ "Build with Cargo and return this package's compiler artifacts."
16
+ cfg = settings(root)['maturin']
17
+ manifest = (root/'Cargo.toml').resolve()
18
+ binding = (root/cfg.get('manifest-path', 'Cargo.toml')).resolve()
19
+ cmd = ['cargo', 'build', '--manifest-path', str(manifest), '--message-format=json-render-diagnostics']
20
+ if library: cmd.append('--lib')
21
+ if binaries: cmd.append('--bins')
22
+ if profile := profile or cfg.get('profile'): cmd += ['--profile', profile]
23
+ if target: cmd += ['--target', target]
24
+ if no_default_features: cmd.append('--no-default-features')
25
+ else:
26
+ if features := cfg.get('features'): cmd += ['--features', ','.join(features)]
27
+ for flag in ('all-features', 'no-default-features'):
28
+ if cfg.get(flag): cmd.append('--' + flag)
29
+ out = subprocess.run(cmd, cwd=root, stdout=subprocess.PIPE, text=True, check=True).stdout
30
+ messages = [json.loads(line) for line in out.splitlines()]
31
+ return [m for m in messages if m.get('reason') == 'compiler-artifact' and Path(m['manifest_path']) in (manifest, binding)]
32
+
33
+
34
+ def replace(source, destination):
35
+ "Replace an installed artifact without changing the file held by running processes."
36
+ destination.parent.mkdir(parents=True, exist_ok=True)
37
+ tmp = destination.with_name(destination.name + '.tmp')
38
+ shutil.copy2(source, tmp)
39
+ os.replace(tmp, destination)
40
+ return destination
41
+
42
+
43
+ def develop(root):
44
+ "Install a Python extension and opted-in binaries from one Cargo build."
45
+ tool = settings(root)
46
+ cfg = tool['maturin']
47
+ if 'python-source' not in cfg: raise SystemExit(r'cargo develop requires tool.maturin.python-source')
48
+ binaries = tool.get('fastws', {}).get('native-binaries', False)
49
+ artifacts = build(root, binaries=binaries, profile='test')
50
+ artifact, = [m for m in artifacts if 'cdylib' in m['target']['crate_types']]
51
+ lib, = [f for f in artifact['filenames'] if Path(f).suffix in ('.dylib', '.so', '.dll')]
52
+ module = cfg.get('module-name', artifact['target']['name'])
53
+ dest = root/cfg['python-source']/Path(*module.split('.'))
54
+ paths = [replace(lib, dest.with_name(dest.name + sysconfig.get_config_var('EXT_SUFFIX')))]
55
+ if binaries:
56
+ env = Path(os.environ.get('VIRTUAL_ENV', sys.prefix))
57
+ scripts = env/('Scripts' if os.name == 'nt' else 'bin')
58
+ paths += [replace(m['executable'], scripts/Path(m['executable']).name) for m in artifacts if m.get('executable')]
59
+ return paths
60
+
61
+
62
+ def stage_binaries(root, profile=None, target=None):
63
+ "Stage Python-free Cargo binaries in maturin's wheel-data directory."
64
+ cfg = settings(root)['maturin']
65
+ dest = root/cfg['data']/'scripts'
66
+ artifacts = build(root, library=False, binaries=True, profile=profile, target=target, no_default_features=True)
67
+ return [replace(m['executable'], dest/Path(m['executable']).name) for m in artifacts if m.get('executable')]
@@ -1,6 +1,6 @@
1
1
  "Fast workspace tools for multi-repo management."
2
2
 
3
- __all__ = ["ws_setup", "ws_clone", "ws_pull", "ws_status", "ws_branches", "ws_build", "ws_sync", "ws_add", "ws_remove"]
3
+ __all__ = ["ws_setup", "ws_clone", "ws_pull", "ws_status", "ws_branches", "ws_build", "ws_sync", "ws_add", "ws_remove", "cargo_develop", "cargo_stage"]
4
4
 
5
5
  import ast, fnmatch, glob, hashlib, json, os, re, shlex, shutil, subprocess, sys, time
6
6
  from pathlib import Path
@@ -176,13 +176,21 @@ def _remove_from_pyproject(pyproject_path: Path, names: list[str]) -> list[str]:
176
176
  pyproject_path.write_text(content)
177
177
  return removed
178
178
 
179
- def _read_pyproject_name(path: Path) -> str|None:
179
+ def _pyproject_name(path: Path) -> str|None:
180
+ "`[project].name` as written, with any `{placeholder}` slots left in"
180
181
  try: data = tomllib.loads(path.read_text())
181
182
  except tomllib.TOMLDecodeError:
182
183
  print(f"Skipping invalid TOML: {path}")
183
184
  return None
184
185
  name = data.get("project", {}).get("name")
185
- return name if isinstance(name, str) and name and "{" not in name and "}" not in name else None
186
+ return name if isinstance(name, str) and name else None
187
+
188
+ def _is_placeholder(name: str|None) -> bool: return bool(name) and ("{" in name or "}" in name)
189
+
190
+ def _read_pyproject_name(path: Path) -> str|None:
191
+ "`[project].name`, or None when it is missing or still a template placeholder"
192
+ name = _pyproject_name(path)
193
+ return None if _is_placeholder(name) else name
186
194
 
187
195
  def _ws_projects(root: Path) -> list[str]:
188
196
  return [name for d in (o for o in _ws_dirs(root) if (o/"pyproject.toml").exists())
@@ -208,6 +216,10 @@ def _non_python_project(d: Path) -> bool:
208
216
  "A Rust or JS project without a Python layer is not a pending Python scaffold."
209
217
  return not (d/"pyproject.toml").exists() and any((d/f).exists() for f in ("Cargo.toml", "package.json"))
210
218
 
219
+ def _template_dir(d: Path) -> bool:
220
+ "A project template. Its `[project].name` still holds `{placeholder}` slots, so uv cannot parse it."
221
+ return (d/"pyproject.toml").exists() and _is_placeholder(_pyproject_name(d/"pyproject.toml"))
222
+
211
223
  def _pending_dirs(root: Path) -> list[str]:
212
224
  "uv workspace dirs that are not Python projects yet; sync stops rather than let uv fail on them"
213
225
  return [d.name for d in _ws_dirs(root) if not (d/"pyproject.toml").exists()]
@@ -218,7 +230,7 @@ def _sync_ws_excludes(pyproject_path: Path, root: Path, tracked: set[str]) -> tu
218
230
  Kept as-is: `[tool.fastws].exclude` entries (intent), globs, missing dirs, and tracked dirs
219
231
  (repos.txt checkouts) that are still not valid Python projects. Auto-managed: entries for other
220
232
  existing dirs are regenerated each sync, excluding dirs without a valid pyproject (tracked dirs
221
- only when they are Cargo-only crates or npm-only packages, since a tracked dir with none of those files is a pending member
233
+ only when they are Cargo-only crates, npm-only packages, or templates with a `{placeholder}` name, since a tracked dir with none of those files is a pending member
222
234
  awaiting scaffolding) and un-excluding dirs that gained one; deliberately excluding a real
223
235
  project takes a `[tool.fastws]` entry."""
224
236
  if not pyproject_path.exists(): return [], []
@@ -231,7 +243,7 @@ def _sync_ws_excludes(pyproject_path: Path, root: Path, tracked: set[str]) -> tu
231
243
  kept = [e for e in cur if e in intent or any(c in e for c in "*?[") or not (root/e).is_dir() or (e in tracked and not _valid_project_dir(root/e))]
232
244
  kept += [e for e in intent if e not in kept]
233
245
  auto = [d.name for d in _project_dirs(root, exclude=kept) if not d.name.startswith(".") and any(_matches_ws(d.name, m) for m in members)
234
- and (d.name not in tracked or _non_python_project(d)) and not _valid_project_dir(d)]
246
+ and (d.name not in tracked or _non_python_project(d) or _template_dir(d)) and not _valid_project_dir(d)]
235
247
  survivors = set(kept) | set(auto)
236
248
  new = [e for e in cur if e in survivors] + [e for e in kept + auto if e not in cur]
237
249
  if new == cur: return [], []
@@ -545,6 +557,26 @@ def _build_projects(root: Path, repos_file: str, project: str = None) -> list[tu
545
557
  dirs = dep_closure(key, graph)
546
558
  return [(n,d) for n,d in res if d in dirs]
547
559
 
560
+ @call_parse(pos=['command'])
561
+ def cargo_develop(
562
+ command: str = 'develop', # Subcommand name supplied by Cargo
563
+ path: str = '.', # Rust/Python project directory
564
+ ):
565
+ r"Build and install a mixed Rust/Python project's extension and opted-in binaries."
566
+ from .cargo import develop
567
+ for dest in develop(Path(path).resolve()): print(f'Installed {dest}')
568
+
569
+ @call_parse(pos=['command'])
570
+ def cargo_stage(
571
+ command: str = 'stage', # Subcommand name supplied by Cargo
572
+ path: str = '.', # Rust/Python project directory
573
+ profile: str = '', # Cargo profile; defaults to the maturin profile or dev
574
+ target: str = '', # Cargo target triple
575
+ ):
576
+ r"Stage Python-free native binaries for maturin wheel builds."
577
+ from .cargo import stage_binaries
578
+ for dest in stage_binaries(Path(path).resolve(), profile or None, target or None): print(f'Staged {dest}')
579
+
548
580
  @call_parse
549
581
  def ws_build(
550
582
  workspace: str = "", # Workspace root; defaults to active venv parent when available
@@ -911,6 +943,9 @@ async def ws_sync(
911
943
  if up: _cargo_update(root, workers=workers)
912
944
  _sync_cargo_keys(root)
913
945
  subprocess.run(["uv", "sync", "-U"] if up else ["uv", "sync"], check=True, cwd=root)
946
+ for _,d in _build_projects(root, repos_file):
947
+ cfg = tomllib.loads((d/'pyproject.toml').read_text()).get('tool', {}).get('maturin', {})
948
+ if 'python-source' in cfg and cfg.get('bindings') != 'bin': cargo_develop(path=str(d))
914
949
  if up: _upgrade_stamp(root).touch()
915
950
  if js_members: _sync_js(root, js_members)
916
951
 
@@ -1,16 +1,16 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fastws-cli
3
- Version: 0.0.18
3
+ Version: 0.0.20
4
4
  Summary: Fast workspace tools for multi-repo management
5
5
  Author: Jeremy Howard
6
- License: Apache-2.0
6
+ License-Expression: Apache-2.0
7
7
  Project-URL: Homepage, https://github.com/AnswerDotAI/fastws
8
8
  Classifier: Programming Language :: Python :: 3
9
9
  Classifier: Programming Language :: Python :: 3 :: Only
10
10
  Requires-Python: >=3.10
11
11
  Description-Content-Type: text/markdown
12
12
  License-File: LICENSE
13
- Requires-Dist: fastcore>=2.2.28
13
+ Requires-Dist: fastcore>=2.2.33
14
14
  Requires-Dist: fastgit>=0.1.2
15
15
  Requires-Dist: ghapi>=2.0.6
16
16
  Requires-Dist: packaging
@@ -195,6 +195,34 @@ ws-build --project solveit # only this package and its transitive workspace dep
195
195
 
196
196
  `--project` selects by package name and follows declared runtime and build dependencies, including workspace packages in external checkouts. Other packages in the output directory are left untouched. Without it, the whole workspace is built as before.
197
197
 
198
+ ### `cargo develop`
199
+
200
+ Rebuild a mixed Rust/Python project's extension and replace it in the editable source package:
201
+
202
+ ```bash
203
+ cargo develop
204
+ cargo develop --path /path/to/project
205
+ ```
206
+
207
+ Run from the project root, or give its directory with `--path`. The command reads `python-source`, `module-name`, `manifest-path` and features from `[tool.maturin]`. It always builds with Cargo's `test` profile, matching bare `cargo test`. The project must produce a `cdylib` and set `python-source`. To use release settings without command-line flags, add `[profile.test]` with `inherits = "release"` to `Cargo.toml`. Keep incremental compilation enabled for the local crate in that profile.
208
+
209
+ 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.
210
+
211
+ Cargo must already build a loadable Python extension. For PyO3 extension-module builds on macOS, configure the linker in `build.rs` with `pyo3_build_config::add_extension_module_link_args()`. This command does not inject maturin's extra compiler flags.
212
+
213
+ To share the library build with `cargo test`, also produce an `rlib` and use the same features for both commands. Rust unit tests compile a separate library with `cfg(test)`; integration tests reuse the ordinary library. `cargo develop` does not change the project's Cargo configuration or its test layout.
214
+
215
+ Set `native-binaries = true` under `[tool.fastws]` to build and install the package's Cargo binary targets into the active venv alongside its extension. Cargo reports the executable paths; there is no separate list of binary names.
216
+
217
+ For wheels containing these binaries, use `fastws.build_backend` as the build backend and include `fastws-cli>=0.0.20` alongside maturin in `[build-system].requires`. Set `[tool.maturin].data` to the wheel-data directory. The backend stages Python-free binaries in its `scripts/` subdirectory, then delegates packaging to maturin. Explicit profile and target arguments from maturin's build settings also apply to the binaries.
218
+
219
+ Direct maturin CLI builds bypass the Python build backend. Stage the binaries first with the shared command:
220
+
221
+ ```bash
222
+ cargo stage --profile dist
223
+ maturin build --profile dist
224
+ ```
225
+
198
226
  ### `ws-sync`
199
227
 
200
228
  Syncing proceeds in this order:
@@ -208,7 +236,7 @@ By default, `ws-sync` uses the active venv's parent as the workspace root. It cr
208
236
 
209
237
  Project scanning respects `tool.uv.workspace.members` and `exclude`. If a member directory lacks `pyproject.toml`, such as a fresh empty clone, sync warns and skips the `uv sync` step.
210
238
 
211
- Fastws maintains `tool.uv.workspace.exclude`. It excludes unlisted top-level directories without a valid Python project. It also excludes listed Rust or JavaScript projects without `pyproject.toml`. For a listed checkout with none of `pyproject.toml`, `Cargo.toml`, or `package.json`, sync warns and skips installation.
239
+ Fastws maintains `tool.uv.workspace.exclude`. It excludes unlisted top-level directories without a valid Python project. It also excludes listed Rust or JavaScript projects without `pyproject.toml`. It also excludes listed project templates, whose `[project].name` still holds a `{placeholder}`, because uv cannot parse that name. For a listed checkout with none of `pyproject.toml`, `Cargo.toml`, or `package.json`, sync warns and skips installation.
212
240
 
213
241
  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.
214
242
 
@@ -224,7 +252,9 @@ Each sync updates `workspaces` in the root `package.json`. It creates the file w
224
252
 
225
253
  An optional tracked `package.json.shared` is merged into the root `package.json` on every sync. Object-valued fields merge one level deep, shared values win conflicts, and arrays replace rather than concatenate. Unrelated local settings remain; removing a shared key does not delete its local value. Workspace discovery still manages `workspaces`. Use this file for shared policy such as version-pinned npm `allowScripts` approvals.
226
254
 
227
- After `uv sync`, fastws runs `<tool> install` at the workspace root. It then runs `<tool> run build` in each JavaScript package containing both `Cargo.toml` and a `build` script. Cargo handles incremental compilation. Other build steps still run.
255
+ After `uv sync`, fastws runs `cargo develop` in each mixed Rust/Python project. Cargo skips unchanged compilation inputs. These projects should keep only packaging inputs in `tool.uv.cache-keys`; Rust source changes are handled by Cargo rather than triggering a separate maturin editable build. Maturin packages with `bindings = "bin"` keep their existing uv installation path.
256
+
257
+ Fastws then runs `<tool> install` at the workspace root. It runs `<tool> run build` in each JavaScript package containing both `Cargo.toml` and a `build` script. Cargo handles incremental compilation. Other build steps still run.
228
258
 
229
259
  For npm, sync hides routine install summaries and build-command banners. Install warnings, audit findings, unreviewed install scripts, and failure diagnostics remain visible. Build scripts retain their own output; configure their tools to suppress routine progress. Other package managers retain their normal output.
230
260
 
@@ -311,4 +341,4 @@ await check_release('mdhtml') # one repo: list of unreleased commit summarie
311
341
 
312
342
  ## Tests
313
343
 
314
- Run `pytest -q` for the regular suite. `pytest -q tests/test_setup.py -m slow` also builds the current package and bootstraps a fresh environment with real Git and uv; it may fetch package dependencies. Run `chkstyle fastws tests` after edits.
344
+ Run `pytest -q`. Keep tests concise and focused on substantive logic. Do not test policy choices, settings, constants, or orchestration. Do not add tests merely because code changed.
@@ -2,6 +2,8 @@ LICENSE
2
2
  README.md
3
3
  pyproject.toml
4
4
  fastws/__init__.py
5
+ fastws/build_backend.py
6
+ fastws/cargo.py
5
7
  fastws/core.py
6
8
  fastws/releases.py
7
9
  fastws_cli.egg-info/PKG-INFO
@@ -1,4 +1,6 @@
1
1
  [console_scripts]
2
+ cargo-develop = fastws.core:cargo_develop
3
+ cargo-stage = fastws.core:cargo_stage
2
4
  ws-add = fastws.core:ws_add
3
5
  ws-branches = fastws.core:ws_branches
4
6
  ws-build = fastws.core:ws_build
@@ -1,4 +1,4 @@
1
- fastcore>=2.2.28
1
+ fastcore>=2.2.33
2
2
  fastgit>=0.1.2
3
3
  ghapi>=2.0.6
4
4
  packaging
@@ -1,5 +1,5 @@
1
1
  [build-system]
2
- requires = ["setuptools>=68", "wheel"]
2
+ requires = ["setuptools>=77"]
3
3
  build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
@@ -8,7 +8,7 @@ dynamic = ["version"]
8
8
  description = "Fast workspace tools for multi-repo management"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
11
- license = { text = "Apache-2.0" }
11
+ license = "Apache-2.0"
12
12
  authors = [{ name = "Jeremy Howard" }]
13
13
  classifiers = [
14
14
  "Programming Language :: Python :: 3",
@@ -16,7 +16,7 @@ classifiers = [
16
16
  ]
17
17
 
18
18
  dependencies = [
19
- "fastcore>=2.2.28",
19
+ "fastcore>=2.2.33",
20
20
  "fastgit>=0.1.2",
21
21
  "ghapi>=2.0.6",
22
22
  "packaging",
@@ -33,6 +33,8 @@ dev = [
33
33
  ]
34
34
 
35
35
  [project.scripts]
36
+ cargo-develop = "fastws.core:cargo_develop"
37
+ cargo-stage = "fastws.core:cargo_stage"
36
38
  ws-setup = "fastws.core:ws_setup"
37
39
  ws-clone = "fastws.core:ws_clone"
38
40
  ws-pull = "fastws.core:ws_pull"
@@ -47,10 +49,6 @@ ws-releases = "fastws.releases:ws_releases"
47
49
  [project.urls]
48
50
  Homepage = "https://github.com/AnswerDotAI/fastws"
49
51
 
50
- [tool.pytest.ini_options]
51
- asyncio_mode = "auto"
52
- markers = ["slow: builds and installs a fresh workspace; may fetch package dependencies"]
53
- addopts = "-m 'not slow'"
54
52
 
55
53
  [tool.setuptools.dynamic]
56
54
  version = { attr = "fastws.__version__" }
@@ -0,0 +1,25 @@
1
+ import pytest
2
+ import fastws.core as core
3
+
4
+
5
+ def _mk_proj(root, name, extra=''):
6
+ d = root/name
7
+ d.mkdir()
8
+ (d/'pyproject.toml').write_text(fr'''[project]
9
+ name = "{name}"
10
+ {extra}
11
+ ''')
12
+ return d
13
+
14
+
15
+ def test_build_dependency_selection(tmp_path):
16
+ root = tmp_path/'ws'
17
+ root.mkdir()
18
+ _mk_proj(root, 'app', r'''dependencies = ["My.Lib>=1"]''')
19
+ _mk_proj(root, 'my-lib', r'''[build-system]
20
+ requires = ["external"]''')
21
+ external = _mk_proj(tmp_path, 'external', r'''dependencies = ["app", "published-only"]''')
22
+ _mk_proj(root, 'unrelated')
23
+ (root/'repos-local.txt').write_text(f'owner/external {external}')
24
+ assert {n for n,d in core._build_projects(root, 'repos.txt', 'APP')} == {'app', 'my-lib', 'external'}
25
+ with pytest.raises(SystemExit, match='No workspace project'): core._build_projects(root, 'repos.txt', 'missing')
@@ -0,0 +1,15 @@
1
+ from fastcore.basics import AttrDict
2
+
3
+ import fastws.releases as relmod
4
+
5
+
6
+ def _rel(tag): return AttrDict(tag_name=tag)
7
+
8
+
9
+ def test_newest_tag_by_version_not_publish_order():
10
+ assert relmod._newest_tag([_rel("v0.1.17"), _rel("v0.1.18"), _rel("v0.1.9")]) == "v0.1.18"
11
+ assert relmod._newest_tag([]) is None
12
+
13
+
14
+
15
+
@@ -0,0 +1,15 @@
1
+ import pytest
2
+ import fastws.core as core
3
+
4
+
5
+ def test_setup_preserves_existing_destinations(tmp_path):
6
+ marker = tmp_path/'keep.txt'
7
+ marker.write_text('personal work')
8
+ with pytest.raises(SystemExit, match='exists'): core.ws_setup('org/base', str(tmp_path))
9
+ assert marker.read_text() == 'personal work'
10
+ link = tmp_path/'link'
11
+ link.symlink_to(tmp_path/'missing')
12
+ with pytest.raises(SystemExit, match='exists'): core.ws_setup('org/base', str(link))
13
+ assert link.is_symlink() and not (tmp_path/'missing').exists()
14
+
15
+