From 4526ff694c76c30c2aea14822b4ff337e14f2ac9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20V=C3=A9ril?= Date: Sun, 8 Nov 2026 17:58:56 +0100 Subject: [PATCH 01/12] Simplify release promotion and documentation deployment (#205) * Simplify release promotion and version-derived documentation * Require a version tag for dispatched binary publication * Format release validation and smoke checks --- tools/check-release.py | 83 +++++++------ tools/prepare-release.py | 6 +- tools/publish-docs.py | 8 +- tools/release_smoke.py | 214 +++++++++++++++++++++++++++------- tools/test_prepare_release.py | 71 +++++++---- tools/test_publish_docs.py | 12 +- tools/test_release_smoke.py | 105 ++++++++++++++--- 7 files changed, 369 insertions(+), 130 deletions(-) diff --git a/tools/check-release.py b/tools/check-release.py index 7f26209b..f1cc3b14 100644 --- a/tools/check-release.py +++ b/tools/check-release.py @@ -12,27 +12,30 @@ def candidate(root): - manifest = tomllib.loads((root / 'Cargo.toml').read_text()) - core = tomllib.loads((root / 'crates/rustiq-core/Cargo.toml').read_text()) - version = manifest['package']['version'] - if not re.fullmatch(r'(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(-[0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*)?', version): - raise ValueError('invalid release version') - pre = version.partition('-')[2].split('.') - if 'dev' in pre: - raise ValueError('development versions cannot be released') - if any(part.isdigit() and len(part) > 1 and part.startswith('0') for part in pre): - raise ValueError('numeric prerelease identifiers cannot have leading zeros') - if core['package']['version'] != version: - raise ValueError('workspace package versions differ') - if manifest['dependencies']['rustiq-core']['version'].removeprefix('=') != version: - raise ValueError('internal dependency must match release version') + manifest = tomllib.loads((root / "Cargo.toml").read_text()) + core = tomllib.loads((root / "crates/rustiq-core/Cargo.toml").read_text()) + version = manifest["package"]["version"] + if not re.fullmatch( + r"(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(-[0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*)?", + version, + ): + raise ValueError("invalid release version") + pre = version.partition("-")[2].split(".") + if "dev" in pre: + raise ValueError("development versions cannot be released") + if any(part.isdigit() and len(part) > 1 and part.startswith("0") for part in pre): + raise ValueError("numeric prerelease identifiers cannot have leading zeros") + if core["package"]["version"] != version: + raise ValueError("workspace package versions differ") + if manifest["dependencies"]["rustiq-core"]["version"].removeprefix("=") != version: + raise ValueError("internal dependency must match release version") return version def lookup(url, token=None): - headers = {'User-Agent': 'RustiQ-release-check', 'Accept': 'application/json'} + headers = {"User-Agent": "RustiQ-release-check", "Accept": "application/json"} if token: - headers['Authorization'] = f'Bearer {token}' + headers["Authorization"] = f"Bearer {token}" try: with urlopen(Request(url, headers=headers), timeout=30) as response: return json.load(response) @@ -43,39 +46,43 @@ def lookup(url, token=None): def available(version, repository): - token = os.environ.get('GH_TOKEN') - base = f'https://api.github.com/repos/{repository}' - for kind in ('git/ref/tags', 'releases/tags'): - if lookup(f'{base}/{kind}/v{version}', token) is not None: - raise ValueError(f'v{version} already exists ({kind})') - for package in ('rustiq-chem-core', 'rustiq-chem'): - if lookup(f'https://crates.io/api/v1/crates/{package}/{version}') is not None: - raise ValueError(f'{package} {version} is already published') - owner, name = repository.split('/') - versions = lookup(f'https://{owner}.github.io/{name}/versions.json') - if versions and any(entry['version'] == f'v{version}' for entry in versions['versions']): - raise ValueError(f'manual v{version} is already published') + token = os.environ.get("GH_TOKEN") + base = f"https://api.github.com/repos/{repository}" + for kind in ("git/ref/tags", "releases/tags"): + if lookup(f"{base}/{kind}/v{version}", token) is not None: + raise ValueError(f"v{version} already exists ({kind})") + for package in ("rustiq-chem-core", "rustiq-chem"): + if lookup(f"https://crates.io/api/v1/crates/{package}/{version}") is not None: + raise ValueError(f"{package} {version} is already published") + owner, name = repository.split("/") + versions = lookup(f"https://{owner}.github.io/{name}/versions.json") + if versions and any( + entry["version"] == f"v{version}" for entry in versions["versions"] + ): + raise ValueError(f"manual v{version} is already published") def main(): parser = argparse.ArgumentParser(description=__doc__) - parser.add_argument('--repository', default='mveril/RustiQ') - parser.add_argument('--tag') - parser.add_argument('--check-availability', action='store_true') - parser.add_argument('--dry-run', action='store_true') + parser.add_argument("--repository", default="mveril/RustiQ") + parser.add_argument("--tag") + parser.add_argument("--check-availability", action="store_true") + parser.add_argument("--dry-run", action="store_true") args = parser.parse_args() try: version = candidate(Path.cwd()) - if args.tag and args.tag != f'v{version}': - raise ValueError('tag does not match package version') + if args.tag and args.tag != f"v{version}": + raise ValueError("tag does not match package version") if args.check_availability: available(version, args.repository) if args.dry_run: - subprocess.run(['cargo', 'publish', '--workspace', '--locked', '--dry-run'], check=True) - print(f'v{version}') + subprocess.run( + ["cargo", "publish", "--workspace", "--locked", "--dry-run"], check=True + ) + print(f"v{version}") except (ValueError, OSError, subprocess.CalledProcessError) as error: - parser.exit(1, f'Release validation failed: {error}\n') + parser.exit(1, f"Release validation failed: {error}\n") -if __name__ == '__main__': +if __name__ == "__main__": main() diff --git a/tools/prepare-release.py b/tools/prepare-release.py index ef6c8d6f..2d5d498f 100644 --- a/tools/prepare-release.py +++ b/tools/prepare-release.py @@ -6,5 +6,7 @@ import runpy from pathlib import Path -if __name__ == '__main__': - runpy.run_path(str(Path(__file__).with_name('check-release.py')), run_name='__main__') +if __name__ == "__main__": + runpy.run_path( + str(Path(__file__).with_name("check-release.py")), run_name="__main__" + ) diff --git a/tools/publish-docs.py b/tools/publish-docs.py index a86a6373..5927242e 100644 --- a/tools/publish-docs.py +++ b/tools/publish-docs.py @@ -171,12 +171,12 @@ def publish(book, site, revision, version=None): shutil.rmtree(alias_dir) redirect(alias_dir / "index.html", f"../{entry['version']}/") preferred = stable or (entries[0] if entries else None) - redirect(site / "index.html", f"{preferred["version"]}/" if preferred else "dev/") + redirect( + site / "index.html", f"{preferred['version']}/" if preferred else "dev/" + ) # Pages serves this file at arbitrary missing URLs. Never resolve its # destination relative to the requested path, or redirect to another 404. - fallback = ( - preferred["version"] if preferred else "dev" - ) + fallback = preferred["version"] if preferred else "dev" redirect(site / "404.html", f"/RustiQ/{fallback}/") output = { "legacy_redirects": True, diff --git a/tools/release_smoke.py b/tools/release_smoke.py index 5edfea3f..5c793ce1 100644 --- a/tools/release_smoke.py +++ b/tools/release_smoke.py @@ -22,32 +22,46 @@ def verify_checksum(archive: Path, checksum_file: Path) -> None: # cargo-dist may insert blank separator lines into .sha256 sidecars. # Ignore only empty/whitespace-only lines; malformed nonempty lines must # continue to fail the strict checksum validation below. - lines = [line for line in checksum_file.read_text(encoding="utf-8").splitlines() if line.strip()] + lines = [ + line + for line in checksum_file.read_text(encoding="utf-8").splitlines() + if line.strip() + ] if not lines: - raise ValueError(f"SHA-256 checksum file contains no entries: {checksum_file.name}") + raise ValueError( + f"SHA-256 checksum file contains no entries: {checksum_file.name}" + ) entries: list[tuple[str, str]] = [] digest_only = len(lines) == 1 and re.fullmatch(r"[0-9a-fA-F]{64}", lines[0]) if digest_only: if checksum_file.resolve() != Path(str(archive) + ".sha256").resolve(): - raise ValueError("digest-only checksum is only valid in the archive's .sha256 sidecar") + raise ValueError( + "digest-only checksum is only valid in the archive's .sha256 sidecar" + ) entries.append((lines[0].lower(), archive.name)) else: for line in lines: # dist's sha256.sum uses the standard sha256sum format. match = re.fullmatch(r"([0-9a-fA-F]{64}) ([ *])(.+)", line) if not match or "\\" in match.group(3): - raise ValueError(f"malformed SHA-256 checksum entry in {checksum_file.name}: {line!r}") + raise ValueError( + f"malformed SHA-256 checksum entry in {checksum_file.name}: {line!r}" + ) entries.append((match.group(1).lower(), match.group(3))) matching = [(value, name) for value, name in entries if name == archive.name] if len(matching) != 1: - raise ValueError(f"expected one exact checksum entry for {archive.name} in {checksum_file.name}, found {len(matching)}") + raise ValueError( + f"expected one exact checksum entry for {archive.name} in {checksum_file.name}, found {len(matching)}" + ) digest = hashlib.sha256() with archive.open("rb") as stream: for chunk in iter(lambda: stream.read(1024 * 1024), b""): digest.update(chunk) if digest.hexdigest() != matching[0][0]: - raise ValueError(f"SHA-256 mismatch for {archive.name} using {checksum_file.name}") + raise ValueError( + f"SHA-256 mismatch for {archive.name} using {checksum_file.name}" + ) def safe_extract(archive: Path, destination: Path) -> None: @@ -63,25 +77,49 @@ def safe_extract(archive: Path, destination: Path) -> None: with tarfile.open(archive, "r:*") as bundle: for member in bundle.getmembers(): target = (destination / member.name).resolve() - if not target.is_relative_to(destination.resolve()) or member.issym() or member.islnk(): + if ( + not target.is_relative_to(destination.resolve()) + or member.issym() + or member.islnk() + ): raise ValueError(f"unsafe archive member: {member.name}") bundle.extractall(destination, filter="data") return raise ValueError(f"unsupported or invalid archive: {archive}") -def run(binary: Path, args: list[str], cwd: Path, env: dict[str, str], log: Path) -> str: - result = subprocess.run([str(binary), *args], cwd=cwd, env=env, text=True, - capture_output=True, check=False) - log.write_text(f"$ {binary.name} {' '.join(args)}\n--- stdout ---\n{result.stdout}\n--- stderr ---\n{result.stderr}\nexit={result.returncode}\n", encoding="utf-8") +def run( + binary: Path, args: list[str], cwd: Path, env: dict[str, str], log: Path +) -> str: + result = subprocess.run( + [str(binary), *args], + cwd=cwd, + env=env, + text=True, + capture_output=True, + check=False, + ) + log.write_text( + f"$ {binary.name} {' '.join(args)}\n--- stdout ---\n{result.stdout}\n--- stderr ---\n{result.stderr}\nexit={result.returncode}\n", + encoding="utf-8", + ) if result.returncode: - raise RuntimeError(f"{binary.name} {' '.join(args)} failed ({result.returncode}); see {log.name}: {result.stderr.strip()}") + raise RuntimeError( + f"{binary.name} {' '.join(args)} failed ({result.returncode}); see {log.name}: {result.stderr.strip()}" + ) return result.stdout -def validate(archive: Path, fixture: Path, geometry: Path, reference_source: Path, log_dir: Path, - checksum: Path | None = None, expected_energy: float | None = None, - command_runner=run) -> dict[str, object]: +def validate( + archive: Path, + fixture: Path, + geometry: Path, + reference_source: Path, + log_dir: Path, + checksum: Path | None = None, + expected_energy: float | None = None, + command_runner=run, +) -> dict[str, object]: archive = archive.resolve() fixture = fixture.resolve() geometry = geometry.resolve() @@ -100,10 +138,15 @@ def validate(archive: Path, fixture: Path, geometry: Path, reference_source: Pat except (OSError, ValueError) as error: raise ValueError(f"CHECKSUM_FAILURE: {error}") from error - match = re.search(r"PYSCF_TOTAL_ENERGY:\s*f64\s*=\s*(-?[0-9._]+)", reference_source.read_text(encoding="utf-8")) + match = re.search( + r"PYSCF_TOTAL_ENERGY:\s*f64\s*=\s*(-?[0-9._]+)", + reference_source.read_text(encoding="utf-8"), + ) if expected_energy is None: if not match: - raise ValueError("could not read H2/STO-3G reference from regression source") + raise ValueError( + "could not read H2/STO-3G reference from regression source" + ) expected_energy = float(match.group(1).replace("_", "")) with tempfile.TemporaryDirectory(prefix="rustiq-release-smoke-") as temporary: @@ -121,22 +164,50 @@ def validate(archive: Path, fixture: Path, geometry: Path, reference_source: Pat relocated = root / "relocated archive with spaces" extracted.rename(relocated) extracted = relocated - binaries = [p for p in extracted.rglob("rustiq*") if p.is_file() and p.name.lower() in {"rustiq", "rustiq.exe"}] + binaries = [ + p + for p in extracted.rglob("rustiq*") + if p.is_file() and p.name.lower() in {"rustiq", "rustiq.exe"} + ] if len(binaries) != 1: - raise ValueError(f"EXTRACTION_FAILURE: expected one rustiq executable in archive, found {len(binaries)}") + raise ValueError( + f"EXTRACTION_FAILURE: expected one rustiq executable in archive, found {len(binaries)}" + ) binary = binaries[0] if os.name != "nt": binary.chmod(binary.stat().st_mode | 0o111) - clean_path = os.pathsep.join([str(Path(os.environ.get("SystemRoot", "C:/Windows")) / "System32") if os.name == "nt" else "/usr/bin", "/bin"]) - env = {"PATH": clean_path, "HOME": str(root), "USERPROFILE": str(root), "APPDATA": str(root / "appdata"), "XDG_CACHE_HOME": str(root / "cache"), "RUSTIQ_DATA_BASIS": str(basis), "RUSTIQ_CACHE_HOME": str(root / "cache"), "RUSTIQ_AUTO_DOWNLOAD": "0", "RUSTIQ_COLOR": "never", "NO_COLOR": "1"} + clean_path = os.pathsep.join( + [ + str(Path(os.environ.get("SystemRoot", "C:/Windows")) / "System32") + if os.name == "nt" + else "/usr/bin", + "/bin", + ] + ) + env = { + "PATH": clean_path, + "HOME": str(root), + "USERPROFILE": str(root), + "APPDATA": str(root / "appdata"), + "XDG_CACHE_HOME": str(root / "cache"), + "RUSTIQ_DATA_BASIS": str(basis), + "RUSTIQ_CACHE_HOME": str(root / "cache"), + "RUSTIQ_AUTO_DOWNLOAD": "0", + "RUSTIQ_COLOR": "never", + "NO_COLOR": "1", + } if os.name == "nt": env["SystemRoot"] = os.environ.get("SystemRoot", "C:\\Windows") try: - help_output = command_runner(binary, ["--help"], work, env, log_dir / "help.log") + help_output = command_runner( + binary, ["--help"], work, env, log_dir / "help.log" + ) except (OSError, RuntimeError, subprocess.SubprocessError) as error: raise RuntimeError(f"EXECUTABLE_STARTUP_FAILURE: {error}") from error if not help_output.strip(): - raise ValueError("EXECUTABLE_STARTUP_FAILURE: rustiq --help produced no output") + raise ValueError( + "EXECUTABLE_STARTUP_FAILURE: rustiq --help produced no output" + ) try: manual = binary.parent / "manual/html" index = manual / "index.html" @@ -160,7 +231,9 @@ def validate(archive: Path, fixture: Path, geometry: Path, reference_source: Pat env, log_dir / "docs-web.log", ).strip() - version_line = command_runner(binary, ["--version"], work, env, log_dir / "docs-version.log").strip() + version_line = command_runner( + binary, ["--version"], work, env, log_dir / "docs-version.log" + ).strip() version = version_line.split()[-1] development = "dev" in version.partition("-")[2].split("+", 1)[0].split(".") channel = "dev" if development else f"v{version}" @@ -170,45 +243,99 @@ def validate(archive: Path, fixture: Path, geometry: Path, reference_source: Pat except (OSError, ValueError, RuntimeError, subprocess.SubprocessError) as error: raise ValueError(f"DOCUMENTATION_FAILURE: {error}") from error try: - version_output = command_runner(binary, ["--version", "--verbose"], work, env, log_dir / "version.log") + version_output = command_runner( + binary, ["--version", "--verbose"], work, env, log_dir / "version.log" + ) except (OSError, RuntimeError, subprocess.SubprocessError) as error: raise RuntimeError(f"EXECUTABLE_STARTUP_FAILURE: {error}") from error - if not re.search(r"^ Git commit\s+(?:[0-9a-f]{40}|[0-9a-f]{64})(?: \(dirty\))?$", version_output, re.MULTILINE): - raise ValueError("EXECUTABLE_STARTUP_FAILURE: packaged executable lacks Git provenance") + if not re.search( + r"^ Git commit\s+(?:[0-9a-f]{40}|[0-9a-f]{64})(?: \(dirty\))?$", + version_output, + re.MULTILINE, + ): + raise ValueError( + "EXECUTABLE_STARTUP_FAILURE: packaged executable lacks Git provenance" + ) try: - command_runner(binary, ["basis", "import", str(fixture)], work, env, log_dir / "basis-import.log") + command_runner( + binary, + ["basis", "import", str(fixture)], + work, + env, + log_dir / "basis-import.log", + ) shutil.copyfile(geometry, work / "h2.xyz") - command_runner(binary, ["init", "h2.xyz", "--output", "calculation.toml", "--hf", "rhf"], work, env, log_dir / "init.log") + command_runner( + binary, + ["init", "h2.xyz", "--output", "calculation.toml", "--hf", "rhf"], + work, + env, + log_dir / "init.log", + ) except (OSError, RuntimeError, subprocess.SubprocessError) as error: raise RuntimeError(f"EXECUTABLE_STARTUP_FAILURE: {error}") from error try: - output = command_runner(binary, ["run", "calculation.toml", "--format", "json", "--no-auto-download", "--color", "never"], work, env, log_dir / "calculation.log") + output = command_runner( + binary, + [ + "run", + "calculation.toml", + "--format", + "json", + "--no-auto-download", + "--color", + "never", + ], + work, + env, + log_dir / "calculation.log", + ) except (OSError, RuntimeError, subprocess.SubprocessError) as error: raise RuntimeError(f"SCIENTIFIC_CALCULATION_FAILURE: {error}") from error if "\x1b[" in output: - raise ValueError("JSON_VALIDATION_FAILURE: ANSI escape sequence found in JSON output") + raise ValueError( + "JSON_VALIDATION_FAILURE: ANSI escape sequence found in JSON output" + ) try: result = json.loads(output) hf = result["calculation"]["hf"] except (json.JSONDecodeError, KeyError, TypeError) as error: - raise ValueError(f"JSON_VALIDATION_FAILURE: invalid calculation JSON: {error}") from error + raise ValueError( + f"JSON_VALIDATION_FAILURE: invalid calculation JSON: {error}" + ) from error if result.get("schema_version") != 1: - raise ValueError(f"JSON_VALIDATION_FAILURE: unexpected schema version: {result.get('schema_version')}") + raise ValueError( + f"JSON_VALIDATION_FAILURE: unexpected schema version: {result.get('schema_version')}" + ) if hf.get("converged") is not True: - raise ValueError("SCIENTIFIC_CALCULATION_FAILURE: HF calculation did not converge") + raise ValueError( + "SCIENTIFIC_CALCULATION_FAILURE: HF calculation did not converge" + ) if hf.get("method") != "RHF": - raise ValueError(f"JSON_VALIDATION_FAILURE: expected RHF, got {hf.get('method')}") + raise ValueError( + f"JSON_VALIDATION_FAILURE: expected RHF, got {hf.get('method')}" + ) try: energy = float(hf["total_energy"]) except (KeyError, TypeError, ValueError) as error: - raise ValueError("JSON_VALIDATION_FAILURE: HF total_energy is missing or nonnumeric") from error + raise ValueError( + "JSON_VALIDATION_FAILURE: HF total_energy is missing or nonnumeric" + ) from error if not math.isfinite(energy): raise ValueError("JSON_VALIDATION_FAILURE: HF total_energy must be finite") if not math.isfinite(expected_energy): - raise ValueError("JSON_VALIDATION_FAILURE: expected regression reference energy must be finite") + raise ValueError( + "JSON_VALIDATION_FAILURE: expected regression reference energy must be finite" + ) if abs(energy - expected_energy) > 1e-5: - raise ValueError(f"SCIENTIFIC_CALCULATION_FAILURE: HF energy {energy:.12f} differs from regression reference {expected_energy:.12f} by more than 1e-5 Eh") - return {"checksum": "verified", "startup": "passed", "h2": f"RHF converged; {energy:.12f} Eh"} + raise ValueError( + f"SCIENTIFIC_CALCULATION_FAILURE: HF energy {energy:.12f} differs from regression reference {expected_energy:.12f} by more than 1e-5 Eh" + ) + return { + "checksum": "verified", + "startup": "passed", + "h2": f"RHF converged; {energy:.12f} Eh", + } def main() -> int: @@ -221,7 +348,14 @@ def main() -> int: parser.add_argument("--logs", type=Path, default=Path("release-smoke-logs")) args = parser.parse_args() try: - status = validate(args.archive, args.fixture, args.geometry, args.reference_source, args.logs, args.checksum) + status = validate( + args.archive, + args.fixture, + args.geometry, + args.reference_source, + args.logs, + args.checksum, + ) print(f"PASS: {args.archive.name}: {status['h2']}; SHA-256 verified") return 0 except Exception as error: # noqa: BLE001 - diagnostic boundary for CI diff --git a/tools/test_prepare_release.py b/tools/test_prepare_release.py index 22719f03..bc001136 100644 --- a/tools/test_prepare_release.py +++ b/tools/test_prepare_release.py @@ -5,62 +5,85 @@ import pytest -candidate = runpy.run_path(str(Path(__file__).with_name('check-release.py')))['candidate'] +candidate = runpy.run_path(str(Path(__file__).with_name("check-release.py")))[ + "candidate" +] def manifests(root, version, core=None, dependency=None): - (root / 'crates/rustiq-core').mkdir(parents=True) - (root / 'Cargo.toml').write_text(f'[package]\nversion = "{version}"\n[dependencies]\nrustiq-core = {{version = "{dependency or version}"}}\n') - (root / 'crates/rustiq-core/Cargo.toml').write_text(f'[package]\nversion = "{core or version}"\n') - - -@pytest.mark.parametrize('version', ['1.2.3-dev', '1.2.3-dev.1', '1.2.3-alpha.1.dev', '1.2.3-beta.01']) + (root / "crates/rustiq-core").mkdir(parents=True) + (root / "Cargo.toml").write_text( + f'[package]\nversion = "{version}"\n[dependencies]\nrustiq-core = {{version = "{dependency or version}"}}\n' + ) + (root / "crates/rustiq-core/Cargo.toml").write_text( + f'[package]\nversion = "{core or version}"\n' + ) + + +@pytest.mark.parametrize( + "version", ["1.2.3-dev", "1.2.3-dev.1", "1.2.3-alpha.1.dev", "1.2.3-beta.01"] +) def test_rejects_development_and_invalid_versions(tmp_path, version): manifests(tmp_path, version) with pytest.raises(ValueError): candidate(tmp_path) -@pytest.mark.parametrize('version', ['1.2.3', '1.2.3-alpha.1', '1.2.3-beta.1']) +@pytest.mark.parametrize("version", ["1.2.3", "1.2.3-alpha.1", "1.2.3-beta.1"]) def test_accepts_release_without_mutation(tmp_path, version): manifests(tmp_path, version) - before = {p: p.read_bytes() for p in tmp_path.rglob('Cargo.toml')} + before = {p: p.read_bytes() for p in tmp_path.rglob("Cargo.toml")} assert candidate(tmp_path) == version assert before == {p: p.read_bytes() for p in before} def test_rejects_mismatched_workspace(tmp_path): - manifests(tmp_path, '1.2.3', core='1.2.4') - with pytest.raises(ValueError, match='versions differ'): + manifests(tmp_path, "1.2.3", core="1.2.4") + with pytest.raises(ValueError, match="versions differ"): candidate(tmp_path) def test_rejects_mismatched_dependency(tmp_path): - manifests(tmp_path, '1.2.3', dependency='1.2.2') - with pytest.raises(ValueError, match='dependency'): + manifests(tmp_path, "1.2.3", dependency="1.2.2") + with pytest.raises(ValueError, match="dependency"): candidate(tmp_path) -@pytest.mark.parametrize('existing', ['git/ref/tags', 'releases/tags', 'rustiq-chem-core', 'rustiq-chem', 'versions.json']) +@pytest.mark.parametrize( + "existing", + [ + "git/ref/tags", + "releases/tags", + "rustiq-chem-core", + "rustiq-chem", + "versions.json", + ], +) def test_rejects_each_existing_publication(monkeypatch, existing): - available = runpy.run_path(str(Path(__file__).with_name('check-release.py')))['available'] + available = runpy.run_path(str(Path(__file__).with_name("check-release.py")))[ + "available" + ] def lookup(url, token=None): if existing not in url: return None - return {'versions': [{'version': 'v1.2.3'}]} if existing == 'versions.json' else {} + return ( + {"versions": [{"version": "v1.2.3"}]} if existing == "versions.json" else {} + ) - monkeypatch.setitem(available.__globals__, 'lookup', lookup) - with pytest.raises(ValueError, match='already'): - available('1.2.3', 'mveril/RustiQ') + monkeypatch.setitem(available.__globals__, "lookup", lookup) + with pytest.raises(ValueError, match="already"): + available("1.2.3", "mveril/RustiQ") def test_lookup_failure_does_not_allow_publication(monkeypatch): - available = runpy.run_path(str(Path(__file__).with_name('check-release.py')))['available'] + available = runpy.run_path(str(Path(__file__).with_name("check-release.py")))[ + "available" + ] def lookup(url, token=None): - raise OSError('service unavailable') + raise OSError("service unavailable") - monkeypatch.setitem(available.__globals__, 'lookup', lookup) - with pytest.raises(OSError, match='unavailable'): - available('1.2.3', 'mveril/RustiQ') + monkeypatch.setitem(available.__globals__, "lookup", lookup) + with pytest.raises(OSError, match="unavailable"): + available("1.2.3", "mveril/RustiQ") diff --git a/tools/test_publish_docs.py b/tools/test_publish_docs.py index b5b55b17..f70d2941 100644 --- a/tools/test_publish_docs.py +++ b/tools/test_publish_docs.py @@ -219,8 +219,12 @@ def git(*args): shutil.copy(root / "tools/check-release.py", tmp_path / "tools") (tmp_path / "crates/rustiq-core").mkdir(parents=True) release_version = "1.0.0-beta.1" if tag == "v1.0.0-beta.1" else "1.0.0" - (tmp_path / "Cargo.toml").write_text(f'[package]\nversion = "{release_version}"\n[dependencies]\nrustiq-core = {{version = "{release_version}"}}\n') - (tmp_path / "crates/rustiq-core/Cargo.toml").write_text(f'[package]\nversion = "{release_version}"\n') + (tmp_path / "Cargo.toml").write_text( + f'[package]\nversion = "{release_version}"\n[dependencies]\nrustiq-core = {{version = "{release_version}"}}\n' + ) + (tmp_path / "crates/rustiq-core/Cargo.toml").write_text( + f'[package]\nversion = "{release_version}"\n' + ) git("add", ".") git("commit", "-m", "Initial main") @@ -230,7 +234,9 @@ def git(*args): git("checkout", "-b", "side") git("commit", "--allow-empty", "-m", "Outside main") if tag == "v1.0.0-beta.1": - (tmp_path / "Cargo.toml").write_text('[package]\nversion = "1.0.0-beta.1"\n[dependencies]\nrustiq-core = {version = "1.0.0-beta.1"}\n') + (tmp_path / "Cargo.toml").write_text( + '[package]\nversion = "1.0.0-beta.1"\n[dependencies]\nrustiq-core = {version = "1.0.0-beta.1"}\n' + ) git("add", "Cargo.toml") git("commit", "--allow-empty", "-m", "Prerelease") result = subprocess.run( diff --git a/tools/test_release_smoke.py b/tools/test_release_smoke.py index 373f90ca..c00d7bfa 100644 --- a/tools/test_release_smoke.py +++ b/tools/test_release_smoke.py @@ -17,7 +17,9 @@ def setUp(self): self.geometry = self.root / "h2.xyz" self.geometry.write_text("2\nH2\nH 0 0 -0.37\nH 0 0 0.37\n") self.reference = self.root / "reference.rs" - self.reference.write_text("const PYSCF_TOTAL_ENERGY: f64 = -1.116_759_307_396_425;") + self.reference.write_text( + "const PYSCF_TOTAL_ENERGY: f64 = -1.116_759_307_396_425;" + ) self.archive = self.root / "bundle.tar.gz" content = self.root / "archive-content" content.mkdir() @@ -35,7 +37,9 @@ def setUp(self): bundle.add(content / "rustiq", arcname="rustiq") bundle.add(manual, arcname="manual/html") self.checksum = self.root / "bundle.tar.gz.sha256" - self.checksum.write_text(hashlib.sha256(self.archive.read_bytes()).hexdigest() + " bundle.tar.gz\n") + self.checksum.write_text( + hashlib.sha256(self.archive.read_bytes()).hexdigest() + " bundle.tar.gz\n" + ) def tearDown(self): self.temp.cleanup() @@ -48,7 +52,11 @@ def execute(binary, args, cwd, env, log): if args == ["--help"]: return "usage: rustiq" if args == ["--version", "--verbose"]: - return "rustiq 0.1.0-alpha.1\n Git commit " + "a" * 40 + "\n" + return ( + "rustiq 0.1.0-alpha.1\n Git commit " + + "a" * 40 + + "\n" + ) if args == ["docs", "--print-path"]: return str(binary.parent / "manual/html/index.html") + "\n" if args == ["docs", "--web", "--print-path"]: @@ -58,15 +66,32 @@ def execute(binary, args, cwd, env, log): raise json_output if json_output is not None: return json_output - return json.dumps({"schema_version": 1, "calculation": {"hf": { - "converged": True, "method": "RHF", "total_energy": -1.116759307396425 - }}}) + return json.dumps( + { + "schema_version": 1, + "calculation": { + "hf": { + "converged": True, + "method": "RHF", + "total_energy": -1.116759307396425, + } + }, + } + ) return "ok" + return execute def validate(self, runner=None): - return validate(self.archive, self.fixture, self.geometry, self.reference, - self.root / "logs", self.checksum, command_runner=runner or self.runner()) + return validate( + self.archive, + self.fixture, + self.geometry, + self.reference, + self.root / "logs", + self.checksum, + command_runner=runner or self.runner(), + ) def test_missing_git_provenance(self): original = self.runner() @@ -105,7 +130,15 @@ def test_missing_executable(self): checksum = Path(str(empty) + ".sha256") checksum.write_text(hashlib.sha256(empty.read_bytes()).hexdigest()) with self.assertRaisesRegex(ValueError, "expected one rustiq"): - validate(empty, self.fixture, self.geometry, self.reference, self.root / "empty-logs", checksum, command_runner=self.runner()) + validate( + empty, + self.fixture, + self.geometry, + self.reference, + self.root / "empty-logs", + checksum, + command_runner=self.runner(), + ) def test_invalid_archive(self): bad = self.root / "bad.zip" @@ -113,7 +146,15 @@ def test_invalid_archive(self): checksum = Path(str(bad) + ".sha256") checksum.write_text(hashlib.sha256(bad.read_bytes()).hexdigest()) with self.assertRaisesRegex(ValueError, "invalid archive"): - validate(bad, self.fixture, self.geometry, self.reference, self.root / "bad-logs", checksum, command_runner=self.runner()) + validate( + bad, + self.fixture, + self.geometry, + self.reference, + self.root / "bad-logs", + checksum, + command_runner=self.runner(), + ) def test_corrupted_checksum(self): self.checksum.write_text("0" * 64) @@ -130,7 +171,9 @@ def test_checksum_exact_filename_and_gnu_format(self): def test_checksum_rejects_ambiguous_and_malformed_entries(self): digest = hashlib.sha256(self.archive.read_bytes()).hexdigest() - self.checksum.write_text(f"{digest} {self.archive.name}\n{digest} *{self.archive.name}\n") + self.checksum.write_text( + f"{digest} {self.archive.name}\n{digest} *{self.archive.name}\n" + ) with self.assertRaisesRegex(ValueError, "found 2"): verify_checksum(self.archive, self.checksum) self.checksum.write_text(f"{digest} {self.archive.name}\nnot a checksum\n") @@ -176,21 +219,38 @@ def test_invalid_json(self): self.validate(self.runner(json_output="{broken")) def test_non_converged(self): - payload = {"schema_version": 1, "calculation": {"hf": {"converged": False, "method": "RHF", "total_energy": -1.1167}}} + payload = { + "schema_version": 1, + "calculation": { + "hf": {"converged": False, "method": "RHF", "total_energy": -1.1167} + }, + } with self.assertRaisesRegex(ValueError, "did not converge"): self.validate(self.runner(json_output=json.dumps(payload))) def test_incorrect_energy(self): - payload = {"schema_version": 1, "calculation": {"hf": {"converged": True, "method": "RHF", "total_energy": -1.0}}} + payload = { + "schema_version": 1, + "calculation": { + "hf": {"converged": True, "method": "RHF", "total_energy": -1.0} + }, + } with self.assertRaisesRegex(ValueError, "differs from regression reference"): self.validate(self.runner(json_output=json.dumps(payload))) def test_nonfinite_energy_values_are_rejected(self): for value in (float("nan"), float("inf"), float("-inf")): with self.subTest(value=value): - payload = {"schema_version": 1, "calculation": {"hf": { - "converged": True, "method": "RHF", "total_energy": value - }}} + payload = { + "schema_version": 1, + "calculation": { + "hf": { + "converged": True, + "method": "RHF", + "total_energy": value, + } + }, + } with self.assertRaisesRegex(ValueError, "must be finite"): self.validate(self.runner(json_output=json.dumps(payload))) @@ -206,9 +266,16 @@ def test_missing_or_nonnumeric_energy_is_rejected(self): def test_nonfinite_reference_energy_is_rejected(self): with self.assertRaisesRegex(ValueError, "reference energy must be finite"): - validate(self.archive, self.fixture, self.geometry, self.reference, - self.root / "logs", self.checksum, expected_energy=float("nan"), - command_runner=self.runner()) + validate( + self.archive, + self.fixture, + self.geometry, + self.reference, + self.root / "logs", + self.checksum, + expected_energy=float("nan"), + command_runner=self.runner(), + ) def test_success(self): result = self.validate() From aef471f042bfa71e4d54afbe887d2eef9acce2d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20V=C3=A9ril?= Date: Mon, 9 Nov 2026 20:10:20 +0100 Subject: [PATCH 02/12] Regenerate third-party notices (#207) --- THIRD-PARTY-NOTICES.sha256 | 2 +- THIRD-PARTY-NOTICES.txt | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/THIRD-PARTY-NOTICES.sha256 b/THIRD-PARTY-NOTICES.sha256 index 7bf78c3d..77e3187e 100644 --- a/THIRD-PARTY-NOTICES.sha256 +++ b/THIRD-PARTY-NOTICES.sha256 @@ -1 +1 @@ -7957da50ab2381af6e809ffa75ae1d8c2981868eb96566adbaf8ab50d5cee6f7 THIRD-PARTY-NOTICES.txt +1019e261ed817234736bc7b4dd0aacb41ffcbbc1c89e66bd974592b651dec752 THIRD-PARTY-NOTICES.txt diff --git a/THIRD-PARTY-NOTICES.txt b/THIRD-PARTY-NOTICES.txt index f85162c6..c652b315 100644 --- a/THIRD-PARTY-NOTICES.txt +++ b/THIRD-PARTY-NOTICES.txt @@ -1,6 +1,6 @@ -Cargo.lock SHA-256: dc2449f6780a47ac33c989902f81aede055f8ba19009c1ab7c44bca26fec444c -Notice inputs SHA-256: c867a1cace3ca670dfb8893a115efe15eac9ea539bee81b5714e99f572fec2d6 -Corresponding source: https://github.com/mveril/RustiQ/releases/download/v0.1.0-alpha.1/rustiq-corresponding-source.tar.gz +Cargo.lock SHA-256: 7f274ca9e7f738d58ec0e46d03be9802569df0512ec91892f4ccd1edbd90ad02 +Notice inputs SHA-256: 8c276309486abfbe214c47d6d52b2f5b63cf45bb0ecdf95bb6a8610d04ff929f +Corresponding source: https://github.com/mveril/RustiQ/releases/download/v0.1.0-alpha.1.dev/rustiq-corresponding-source.tar.gz RustiQ third-party licenses ========================== @@ -8220,8 +8220,8 @@ limitations under the License. Apache License 2.0 (Apache-2.0) Used by: - rustiq-chem 0.1.0-alpha.1 — https://github.com/mveril/RustiQ - rustiq-chem-core 0.1.0-alpha.1 — https://github.com/mveril/RustiQ + rustiq-chem 0.1.0-alpha.1.dev — https://github.com/mveril/RustiQ + rustiq-chem-core 0.1.0-alpha.1.dev — https://github.com/mveril/RustiQ Apache License Version 2.0, January 2004 From ac31bf7886771cd08d9646152f55af4025a8960f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20V=C3=A9ril?= Date: Mon, 9 Nov 2026 21:02:16 +0100 Subject: [PATCH 03/12] Add useful project badges to README (#208) --- README.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 2f2b2065..3a6bc891 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,10 @@ # RustiQ -[![CI](https://github.com/mveril/RustiQ/actions/workflows/ci.yml/badge.svg)](https://github.com/mveril/RustiQ/actions/workflows/ci.yml) +[![CI](https://github.com/mveril/RustiQ/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/mveril/RustiQ/actions/workflows/ci.yml?query=branch%3Amain) +[![GitHub release](https://img.shields.io/github/v/release/mveril/RustiQ?include_prereleases)](https://github.com/mveril/RustiQ/releases) +[![crates.io](https://img.shields.io/crates/v/rustiq-chem)](https://crates.io/crates/rustiq-chem) +[![Manual](https://img.shields.io/badge/docs-manual-blue)](https://mveril.github.io/RustiQ/) +[![License: MIT OR Apache-2.0](https://img.shields.io/badge/license-MIT%20OR%20Apache--2.0-blue)](DISTRIBUTION.md) RustiQ is an experimental Rust quantum chemistry application and reusable `rustiq-chem-core` library. RustiQ-authored source is licensed under MIT OR From 1272e8f08a36ed02a87867e2ac62db6485fff223 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20V=C3=A9ril?= Date: Tue, 10 Nov 2026 20:04:11 +0100 Subject: [PATCH 04/12] Test final distributable packages in release smoke workflow (#209) --- .github/workflows/release-smoke.yml | 225 +++++++++++++++++++--------- dist-workspace.toml | 2 +- docs/src/development/releasing.md | 23 ++- 3 files changed, 171 insertions(+), 79 deletions(-) diff --git a/.github/workflows/release-smoke.yml b/.github/workflows/release-smoke.yml index 0e4d4ab4..dccfa305 100644 --- a/.github/workflows/release-smoke.yml +++ b/.github/workflows/release-smoke.yml @@ -18,7 +18,7 @@ jobs: with: ref: ${{ inputs.ref }} - smoke: + build: needs: manual name: ${{ matrix.target }} strategy: @@ -32,7 +32,7 @@ jobs: - target: x86_64-apple-darwin runner: macos-15-intel - target: aarch64-apple-darwin - runner: macos-14 + runner: macos-latest - target: x86_64-pc-windows-msvc runner: windows-2025 - target: aarch64-pc-windows-msvc @@ -120,93 +120,174 @@ jobs: python -m json.tool dist-manifest.json > /dev/null python -c 'import json; from pathlib import Path; files=json.loads(Path("dist-manifest.json").read_text())["upload_files"]; assert isinstance(files, list) and files, "dist manifest has no upload_files"' mkdir -p target/distrib - cp dist-manifest.json target/distrib/dist-manifest.json + cp dist-manifest.json "target/distrib/${{ matrix.target }}-dist-manifest.json" + + - name: Upload distributable packages + id: packages + if: success() + uses: actions/upload-artifact@v7.0.2 + with: + name: rustiq-packages-${{ matrix.target }} + path: | + target/distrib/*.tar.gz + target/distrib/*.zip + target/distrib/*.sha256 + target/distrib/sha256.sum + target/distrib/*-dist-manifest.json + if-no-files-found: error + + - name: Link downloadable packages + if: steps.packages.outcome == 'success' + shell: bash + env: + PACKAGE_URL: ${{ steps.packages.outputs.artifact-url }} + TARGET: ${{ matrix.target }} + run: | + printf '[Download packages for %s](%s) (archive and checksums)\n' "$TARGET" "$PACKAGE_URL" >> "$GITHUB_STEP_SUMMARY" + + - name: Upload archive, manifest, and diagnostic logs + if: always() + uses: actions/upload-artifact@v7.0.2 + with: + name: release-build-${{ matrix.target }} + path: | + target/distrib/** + dist-manifest.json + dist-build.log + validation.log + release-smoke-logs/** + if-no-files-found: warn + + global: + name: Assemble release packages with dist + needs: build + runs-on: ubuntu-22.04 + steps: + - uses: actions/checkout@v7 + with: + ref: ${{ inputs.ref || github.sha }} + persist-credentials: false + + - uses: dtolnay/rust-toolchain@stable + + - name: Install configured dist version + shell: bash + run: | + set -euo pipefail + DIST_VERSION="$(python3 -c 'import re; from pathlib import Path; print(re.search(r"cargo-dist-version\s*=\s*\"([^\"]+)\"", Path("dist-workspace.toml").read_text()).group(1))')" + curl --proto '=https' --tlsv1.2 -LsSf "https://github.com/axodotdev/cargo-dist/releases/download/v${DIST_VERSION}/cargo-dist-installer.sh" | sh + + - name: Download native packages + uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2 + with: + pattern: rustiq-packages-* + path: target/distrib + merge-multiple: true + + - name: Build global release assets + shell: bash + run: | + set -euo pipefail + dist build --artifacts=global --output-format=json > dist-manifest.json + dist print-upload-files-from-manifest --manifest dist-manifest.json > global-upload-files.txt + mkdir -p release-packages + cp target/distrib/*.tar.gz target/distrib/*.zip target/distrib/*.sha256 release-packages/ + while IFS= read -r asset; do + cp "$asset" release-packages/ + done < global-upload-files.txt - - id: validate - name: Validate archive and run H2 calculation + - name: Upload complete release packages + id: release + uses: actions/upload-artifact@v7.0.2 + with: + name: rustiq-release-packages + path: release-packages/* + if-no-files-found: error + + - name: Link complete release download + env: + PACKAGE_URL: ${{ steps.release.outputs.artifact-url }} + run: | + printf '[Download all release packages](%s) — native archives, checksums, and corresponding source.\n' "$PACKAGE_URL" >> "$GITHUB_STEP_SUMMARY" + + smoke: + name: Smoke final packages (${{ matrix.target }}) + needs: global + strategy: + fail-fast: false + matrix: + include: + - target: x86_64-unknown-linux-gnu + runner: ubuntu-22.04 + - target: aarch64-unknown-linux-gnu + runner: ubuntu-24.04-arm + - target: x86_64-apple-darwin + runner: macos-15-intel + - target: aarch64-apple-darwin + runner: macos-latest + - target: x86_64-pc-windows-msvc + runner: windows-2025 + - target: aarch64-pc-windows-msvc + runner: windows-11-arm + runs-on: ${{ matrix.runner }} + steps: + - uses: actions/checkout@v7 + with: + ref: ${{ inputs.ref || github.sha }} + persist-credentials: false + + - name: Download final release packages + uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2 + with: + name: rustiq-release-packages + path: release-packages + + - name: Validate final archive and run H2 calculation shell: bash + env: + TARGET: ${{ matrix.target }} run: | set -euo pipefail - python - <<'PY' 2>&1 | tee validation.log - import json + python - <<'PYTHON' 2>&1 | tee validation.log + import os + import subprocess + import sys from pathlib import Path - manifest = json.loads(Path("target/distrib/dist-manifest.json").read_text()) - paths = [Path(p) for p in manifest["upload_files"]] - archives = [p for p in paths if p.suffix.lower() in {".zip", ".gz"} and "rustiq" in p.name.lower()] + + root = Path("release-packages") + target = os.environ["TARGET"] + suffix = ".zip" if "windows" in target else ".tar.gz" + archives = list(root.glob(f"*-{target}{suffix}")) if len(archives) != 1: - raise SystemExit(f"MISSING_ARCHIVE: expected one RustiQ archive in dist manifest, got {archives}") - archive = archives[0] - checksum = Path(str(archive) + ".sha256") - if not checksum.exists(): - checksum = archive.parent / "sha256.sum" - if not checksum.exists(): - raise SystemExit(f"CHECKSUM_FAILURE: no checksum sidecar or sha256.sum for {archive}") - import subprocess, sys - command = [sys.executable, "tools/release_smoke.py", str(archive), "--fixture", "tests/data/sto-3g.json", "--geometry", "samples/h2/molecule.xyz", "--reference-source", "crates/rustiq-core/src/hf/scf.rs", "--logs", "release-smoke-logs"] - command += ["--checksum", str(checksum)] - subprocess.run(command, check=True) - print(f"::notice title=Archive::{archive.name}") - PY - - - name: Report target result + raise SystemExit(f"MISSING_ARCHIVE: expected one archive for {target}, got {archives}") + checksum = root / "sha256.sum" + if not checksum.is_file(): + raise SystemExit("CHECKSUM_FAILURE: final release has no sha256.sum") + subprocess.run([ + sys.executable, "tools/release_smoke.py", str(archives[0]), + "--checksum", str(checksum), + "--fixture", "tests/data/sto-3g.json", + "--geometry", "samples/h2/molecule.xyz", + "--reference-source", "crates/rustiq-core/src/hf/scf.rs", + "--logs", "release-smoke-logs", + ], check=True) + PYTHON + + - name: Report final package result if: always() shell: bash env: TARGET: ${{ matrix.target }} - INSTALL_UNIX: ${{ steps.install_unix.outcome }} - INSTALL_WINDOWS_X64: ${{ steps.install_windows_x64.outcome }} - INSTALL_WINDOWS_ARM64: ${{ steps.install_windows_arm64.outcome }} - DIST_VERSION_RESULT: ${{ steps.verify_dist.outcome }} - BUILD_RESULT: ${{ steps.build.outcome }} - VALIDATION_RESULT: ${{ steps.validate.outcome }} + RESULT: ${{ job.status }} run: | - case "$TARGET" in - *windows*) archive="rustiq-chem-$TARGET.zip" ;; - *) archive="rustiq-chem-$TARGET.tar.gz" ;; - esac - stage="" - if [[ "$VALIDATION_RESULT" == success ]]; then - checksum=verified; startup=passed; manual=passed; h2="RHF regression check passed"; status=PASS - else - checksum="not confirmed"; startup="not confirmed"; manual="not confirmed"; h2="not confirmed"; status=FAIL - if [[ "$INSTALL_UNIX" == failure || "$INSTALL_WINDOWS_X64" == failure || "$INSTALL_WINDOWS_ARM64" == failure ]]; then - stage="dist installation failure"; archive="not produced" - elif [[ "$DIST_VERSION_RESULT" == failure ]]; then - stage="dist version verification failure"; archive="not produced" - elif [[ "$BUILD_RESULT" == failure ]]; then - stage="archive build failure"; archive="not produced" - elif [[ "$BUILD_RESULT" == skipped ]]; then - stage="setup or prerequisite failure"; archive="not produced" - elif grep -q 'MISSING_ARCHIVE' validation.log 2>/dev/null; then stage="missing archive" - elif grep -q 'CHECKSUM_FAILURE' validation.log 2>/dev/null; then stage="checksum failure" - elif grep -q 'EXTRACTION_FAILURE' validation.log 2>/dev/null; then stage="extraction failure" - elif grep -q 'EXECUTABLE_STARTUP_FAILURE' validation.log 2>/dev/null; then stage="executable startup failure" - elif grep -q 'DOCUMENTATION_FAILURE' validation.log 2>/dev/null; then stage="offline manual failure" - elif grep -q 'SCIENTIFIC_CALCULATION_FAILURE' validation.log 2>/dev/null; then stage="scientific calculation failure" - elif grep -q 'JSON_VALIDATION_FAILURE' validation.log 2>/dev/null; then stage="JSON validation failure" - else stage="validation failure (inspect validation.log)" - fi - fi - { - printf '## Release rehearsal: %s\n\n' "$TARGET" - echo '| Target | Archive | Checksum | Executable startup | Offline manual | H2 calculation | Final status |' - echo '|---|---|---|---|---|---|---|' - printf '| `%s` | `%s` | %s | %s | %s | %s | **%s** |\n' "$TARGET" "$archive" "$checksum" "$startup" "$manual" "$h2" "$status" - if [[ "$status" == FAIL ]]; then printf '\nFailed stage: %s\n' "$stage"; fi - echo - echo "Built and tested on a GitHub-hosted native runner. This is not a pristine OS image." - } >> "$GITHUB_STEP_SUMMARY" - [[ "$VALIDATION_RESULT" == success ]] + printf 'Final release package smoke test: `%s` — **%s**\n' "$TARGET" "$RESULT" >> "$GITHUB_STEP_SUMMARY" - - name: Upload archive, manifest, and diagnostic logs + - name: Upload smoke diagnostics if: always() uses: actions/upload-artifact@v7.0.2 with: name: release-smoke-${{ matrix.target }} path: | - target/distrib/** - dist-manifest.json - dist-build.log validation.log release-smoke-logs/** if-no-files-found: warn diff --git a/dist-workspace.toml b/dist-workspace.toml index 2979c11b..d0518f36 100644 --- a/dist-workspace.toml +++ b/dist-workspace.toml @@ -40,7 +40,7 @@ build = ["python3", "tools/distribution.py", "source"] runner = "ubuntu-24.04-arm" [dist.github-custom-runners.aarch64-apple-darwin] -runner = "macos-14" +runner = "macos-latest" [dist.github-custom-runners.aarch64-pc-windows-msvc] runner = "windows-11-arm" diff --git a/docs/src/development/releasing.md b/docs/src/development/releasing.md index a850465f..5013d9d5 100644 --- a/docs/src/development/releasing.md +++ b/docs/src/development/releasing.md @@ -19,7 +19,7 @@ cannot enter the publishing jobs. All six target builds use native GitHub-hosted runners: Linux x64 on `ubuntu-22.04`, Linux ARM64 on `ubuntu-24.04-arm`, macOS Intel on -`macos-15-intel`, Apple Silicon on `macos-14`, Windows x64 on `windows-2025`, +`macos-15-intel`, Apple Silicon on `macos-latest`, Windows x64 on `windows-2025`, and Windows ARM64 on `windows-11-arm`. The configuration explicitly selects the ARM runners and preserves the native Windows runners. @@ -44,17 +44,28 @@ GitHub-hosted runner. It extracts that archive, verifies its dist-generated SHA-256 checksum, starts the packaged `rustiq` executable, checks `--help`, and runs the checked-in H2/STO-3G regression with a copied local basis fixture and JSON output. Per-target results appear in the job summary; archives, dist -manifests, and command logs are uploaded as workflow artifacts. +manifests, and command logs are uploaded as workflow artifacts. Successful targets also +publish a separate `rustiq-packages-` artifact containing the distributable +archive and checksums. Follow the download link in the run summary, then unzip +the GitHub Actions artifact to obtain the original release archive. Downloads +require a GitHub login and remain available until the workflow artifacts expire. +After all six native builds pass, a global job runs `dist build --artifacts=global`, +just as publication does. This builds the corresponding source archive and combined +checksums. Download `rustiq-release-packages` from its summary link to obtain +all native archives and global assets together. Only after this upload do the +six native smoke jobs download that same artifact and test their target archive +against its combined `sha256.sum`. Thus the downloadable final packages are +the packages exercised by the smoke tests. The source archive requires a +clean committed checkout; commit local changes before rehearsing them in CI. +The Apple Silicon smoke job uses the native `macos-latest` runner. This checks archive integrity and basic executable operation for the six configured targets. The H2 energy comparison reuses the regression reference and is a packaging smoke test, not broad scientific validation, independent platform validation, or proof of compatibility with every supported operating system. The runs use GitHub-hosted runners, which are not pristine operating -system installations. A `sha256.sum` release manifest may only be created in -the tagged release's combined build; when unavailable, the rehearsal verifies -the per-archive checksum emitted by dist and fails if neither checksum is -available. A checksum made during a rehearsal does not independently validate +system installations. The rehearsal requires the combined `sha256.sum` generated by the global +dist build and verifies each tested archive against it. A checksum made during a rehearsal does not independently validate a published release. The rehearsal has read-only repository permissions and only a From 57796f504c475173818284c98f686a0d1240b857 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20V=C3=A9ril?= Date: Tue, 10 Nov 2026 21:40:41 +0100 Subject: [PATCH 05/12] Harmonize hydrogen symbols in XYZ examples (#210) --- crates/rustiq-core/tests/data/integrals/pyscf-v1.json | 2 +- crates/rustiq-core/tests/data/samples/h2/molecule.xyz | 2 +- docs/src/user-guide/geometry.md | 6 ++++++ samples/h2/molecule.xyz | 4 ++-- 4 files changed, 10 insertions(+), 4 deletions(-) diff --git a/crates/rustiq-core/tests/data/integrals/pyscf-v1.json b/crates/rustiq-core/tests/data/integrals/pyscf-v1.json index 5b488d2d..de9d5eb6 100644 --- a/crates/rustiq-core/tests/data/integrals/pyscf-v1.json +++ b/crates/rustiq-core/tests/data/integrals/pyscf-v1.json @@ -30,7 +30,7 @@ ], "rotation": null, "basis_sha256": "6e9edeb7ae5ee1c4155dbc84b8bad9b74f07077f8136c7531f86d7f0d358b638", - "geometry_sha256": "7d00f0a54ca909dd51534e6ec97c532b4a5070382a6a10d2c6613f692a62c08e", + "geometry_sha256": "d00228efce5d72638f7e5bf15b7e0327790a79b10b183676f6878dbffa3efcad", "arrays": { "S": "h2-sto-3g/S", "T": "h2-sto-3g/T", diff --git a/crates/rustiq-core/tests/data/samples/h2/molecule.xyz b/crates/rustiq-core/tests/data/samples/h2/molecule.xyz index 9b9eac2a..e7d50181 100644 --- a/crates/rustiq-core/tests/data/samples/h2/molecule.xyz +++ b/crates/rustiq-core/tests/data/samples/h2/molecule.xyz @@ -1,4 +1,4 @@ 2 Hydrogen molecule -1 0.0 0.0 -0.37 +H 0.0 0.0 -0.37 H 0.0 0.0 0.37 diff --git a/docs/src/user-guide/geometry.md b/docs/src/user-guide/geometry.md index 30f3f8f7..d1f84fb4 100644 --- a/docs/src/user-guide/geometry.md +++ b/docs/src/user-guide/geometry.md @@ -14,6 +14,12 @@ checked-in H₂ example: {{#include ../../../samples/h2/molecule.xyz}} ``` +> [!TIP] +> **Element symbols and atomic numbers are interchangeable.** +> Each atom line may use either an element symbol, such as `H`, or its atomic +> number, such as `1`. Both forms identify hydrogen; this book uses symbols +> consistently for readability. + The atom count must not exceed the number of atom lines present in the file. RustiQ reports a geometry parse error when the count and available lines do not match, including when the count is too large to represent the supplied diff --git a/samples/h2/molecule.xyz b/samples/h2/molecule.xyz index b0c8a798..e7d50181 100644 --- a/samples/h2/molecule.xyz +++ b/samples/h2/molecule.xyz @@ -1,4 +1,4 @@ 2 Hydrogen molecule -1 0.0 0.0 -0.37 -H 0.0 0.0 0.37 \ No newline at end of file +H 0.0 0.0 -0.37 +H 0.0 0.0 0.37 From e3d1121ff419e98cfdc25429cf8a26484cf6e85d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20V=C3=A9ril?= Date: Wed, 11 Nov 2026 17:57:57 +0100 Subject: [PATCH 06/12] Document Home Manager Nix installation (#211) --- build.rs | 18 +- docs/src/SUMMARY.md | 1 + docs/src/development/nix-packages.md | 5 + docs/src/development/nix-setup.md | 10 +- docs/src/getting-started/installation-nix.md | 174 +++++++++++++++++++ docs/src/getting-started/installation.md | 5 + docs/src/getting-started/rustiq.nix | 7 + flake.nix | 18 ++ 8 files changed, 236 insertions(+), 2 deletions(-) create mode 100644 docs/src/getting-started/installation-nix.md create mode 100644 docs/src/getting-started/rustiq.nix diff --git a/build.rs b/build.rs index 7bcefe68..e3a06aa7 100644 --- a/build.rs +++ b/build.rs @@ -56,7 +56,7 @@ fn emit_build_info(manifest_dir: &std::path::Path) -> Result<(), Box> .add_instructions(&rustc)? .emit()?; - provenance::git_instructions(manifest_dir)?.emit()?; + emit_git_provenance(manifest_dir)?; for name in [ "RUSTIQ_BUILD_GIT_COMMIT", @@ -96,6 +96,22 @@ fn emit_build_info(manifest_dir: &std::path::Path) -> Result<(), Box> Ok(()) } +fn emit_git_provenance(manifest_dir: &std::path::Path) -> Result<(), Box> { + let sha = env::var("VERGEN_GIT_SHA").unwrap_or_default(); + let dirty = env::var("VERGEN_GIT_DIRTY").unwrap_or_default(); + + if !sha.is_empty() && !dirty.is_empty() { + println!("cargo:rerun-if-env-changed=VERGEN_GIT_SHA"); + println!("cargo:rerun-if-env-changed=VERGEN_GIT_DIRTY"); + println!("cargo:rustc-env=VERGEN_GIT_SHA={sha}"); + println!("cargo:rustc-env=VERGEN_GIT_DIRTY={dirty}"); + } else { + provenance::git_instructions(manifest_dir)?.emit()?; + } + + Ok(()) +} + fn emit_declared_profile_settings(manifest_dir: &std::path::Path) -> Result<(), Box> { use std::str::FromStr; diff --git a/docs/src/SUMMARY.md b/docs/src/SUMMARY.md index 8fed6f28..c43f7229 100644 --- a/docs/src/SUMMARY.md +++ b/docs/src/SUMMARY.md @@ -5,6 +5,7 @@ # Getting started - [Installation](getting-started/installation.md) + - [Installing with Nix](getting-started/installation-nix.md) - [First H₂ calculation](getting-started/first-calculation.md) - [Interpreting output](getting-started/output.md) diff --git a/docs/src/development/nix-packages.md b/docs/src/development/nix-packages.md index ad27d39e..992060bb 100644 --- a/docs/src/development/nix-packages.md +++ b/docs/src/development/nix-packages.md @@ -1,5 +1,10 @@ # Nix packages and shells +The flake also exposes `legacyPackages..cpuTarget`, a function accepting +an LLVM CPU name and returning a CPU-specific RustiQ package. See the +[installation guide](../getting-started/installation.md#install-with-nix) for +commands and portability considerations. The default package remains portable. + The packages below are declared in [`flake.nix`](https://github.com/mveril/RustiQ/blob/main/flake.nix). All four shells share Rust build essentials. `rust` adds Rust development tools; `mini-pyscf` adds the Python reference-test environment to the minimal Rust diff --git a/docs/src/development/nix-setup.md b/docs/src/development/nix-setup.md index 65ea8c96..9061249e 100644 --- a/docs/src/development/nix-setup.md +++ b/docs/src/development/nix-setup.md @@ -73,7 +73,15 @@ Choose the case that matches your machine: instructions](https://nixos.org/download/) for your platform, restart the shell if requested, and verify the installation with `nix --version`. The official page recommends a multi-user installation when the platform supports - it. + it. To enable the commands used here, add the following line to your user + configuration file, `~/.config/nix/nix.conf` (create the directory and file + if needed): + + ```text + experimental-features = nix-command flakes + ``` + + Merge these features with any existing `experimental-features` setting. In either case, Nix reads `flake.nix` and `flake.lock` from this repository to create the same project-specific toolchain without installing those development diff --git a/docs/src/getting-started/installation-nix.md b/docs/src/getting-started/installation-nix.md new file mode 100644 index 00000000..a9709ea0 --- /dev/null +++ b/docs/src/getting-started/installation-nix.md @@ -0,0 +1,174 @@ +# Installing with Nix + +Install [Nix](https://nixos.org/download/) and enable the `nix-command` and +`flakes` experimental features. The flake supports x86-64 Linux, ARM64 Linux, +and Apple Silicon macOS. On Windows, use Nix inside WSL2 to run the Linux +package. See [Nix setup](../development/nix-setup.md#getting-nix) for enabling +flakes and platform prerequisites. The first install needs network access and +may compile dependencies. Install the standard package from any directory with: + +```sh +nix profile install github:mveril/RustiQ +rustiq --version +``` + +## Home Manager without a configuration flake + +If your Home Manager configuration uses `home.nix` directly, create a +`rustiq.nix` file beside it: + +```nix +{ pkgs, ... }: +let + rustiq = builtins.getFlake "github:mveril/RustiQ"; +in +{ + home.packages = [ + (rustiq.legacyPackages.${pkgs.stdenv.hostPlatform.system}.cpuTarget "native") + ]; +} +``` + +Add the module to the imports in `home.nix`, keeping any existing imports: + +```nix +imports = [ ./rustiq.nix ]; +``` + +Apply the configuration using your usual command. For Home Manager integrated +as a NixOS module, run `sudo nixos-rebuild switch --impure`; for standalone Home +Manager, run `home-manager switch --impure`. The unlocked GitHub reference +requires impure evaluation. Check `rustiq --version` afterward. The reference tracks the default branch +and is not pinned by a configuration `flake.lock`; use a revision in the URL +for a fixed version. See the [CPU-specific build](#optional-cpu-specific-build) +caveats before using `native`. To use the standard package, replace the entry +in `home.packages` with `rustiq.packages.${pkgs.stdenv.hostPlatform.system}.default`. + +## Home Manager and NixOS + +The following snippets extend an existing **flake-based** configuration. Add +RustiQ as an input to your configuration's `flake.nix`: + +```nix +inputs.rustiq.url = "github:mveril/RustiQ"; +``` + +The configuration's `flake.lock` pins that input. To update it later, run +`nix flake update rustiq` in the configuration directory, then apply the +configuration again. Keep your existing inputs, modules, and state version. +If the configuration is a Git flake, add any newly created module files to Git +before applying it so that Nix can see them. + +### Home Manager + +In the existing `outputs` function, bind the inputs as `inputs@{ ... }`, and +pass them to your existing `home-manager.lib.homeManagerConfiguration`: + +```nix +extraSpecialArgs = { inherit inputs; }; +``` + +Merge this with any existing `extraSpecialArgs`. In your `home.nix`, add +`inputs` to the module arguments and add RustiQ to `home.packages`: + +```nix +{ pkgs, inputs, ... }: +{ + home.packages = [ + inputs.rustiq.packages.${pkgs.stdenv.hostPlatform.system}.default + ]; +} +``` + +Merge the list with your existing packages. From your configuration directory, +apply it with `home-manager switch --flake .#YOUR_USER` and check +`rustiq --version`. Replace `YOUR_USER` with your Home Manager configuration +name. See the [Home Manager flake guide](https://nix-community.github.io/home-manager/nix-flakes/standalone.html). +For Home Manager as a NixOS module, pass the inputs through +`home-manager.extraSpecialArgs` instead. + +### NixOS configuration.nix + +In the existing `outputs` function, bind the inputs as `inputs@{ ... }`, and +pass them to your existing `nixpkgs.lib.nixosSystem`: + +```nix +specialArgs = { inherit inputs; }; +``` + +Merge this with any existing `specialArgs`. Create `rustiq.nix` beside your +`configuration.nix`. This complete NixOS module installs a RustiQ build +optimized for the build machine's CPU: + +```nix +{{#include rustiq.nix}} +``` + +Then import it in the existing `configuration.nix`, preserving any other imports: + +```nix +{ ... }: +{ + imports = [ ./rustiq.nix ]; +} +``` + +Alternatively, add `./rustiq.nix` to the `modules` list in `nixosSystem`. +See the CPU caveats below before using `native`. To use the standard package, +replace the expression inside the list with: + +```nix +inputs.rustiq.packages.${pkgs.stdenv.hostPlatform.system}.default +``` + +From the configuration directory, apply it with +`sudo nixos-rebuild switch --flake .#YOUR_HOST` and check `rustiq --version`. +Replace `YOUR_HOST` with your NixOS configuration name. See the +[NixOS package management guide](https://nixos.org/manual/nixos/stable/#sec-declarative-package-management). + +## Optional CPU-specific build + +The NixOS module above uses this expression. For Home Manager, you can also +replace the standard package entry in `home.packages` with it: + +```nix +(inputs.rustiq.legacyPackages.${pkgs.stdenv.hostPlatform.system}.cpuTarget "native") +``` + +For a profile installation from any directory, use one command instead: + +```sh +nix profile install --impure --expr '(builtins.getFlake "github:mveril/RustiQ").legacyPackages.${builtins.currentSystem}.cpuTarget "native"' +``` + +List compiler CPU names with `rustc --print target-cpus` (for example inside +`nix develop github:mveril/RustiQ#rust`). `native` selects the **build machine's CPU**, which may differ from the machine +running RustiQ when using remote builders. Its detected CPU is not recorded in +the Nix build identity: cached results may have been built for a different CPU. +For reproducible builds or deployment across machines, replace `native` with a +compatible explicit CPU name such as `znver3`. A binary may fail on machines +with fewer processor features. This setting applies to Rust-compiled code and +does not promise optimization of every native dependency. The `--impure` flag +allows the command-line expression to obtain the current system and resolve the +unlocked GitHub reference. + +## Build from a checkout + +From the root of a RustiQ checkout, build the default package and run it +without adding it to your profile: + +```sh +nix build +./result/bin/rustiq --version +``` + +The package includes the offline manual and shell completions. Run `rustiq docs` +after installing, or `./result/bin/rustiq docs` from the checkout. See the +[manual instructions](installation.md#consult-the-manual) and the +[Nix package guide](../development/nix-packages.md#installed-manual-and-completions). + +The package enables online basis downloads but does not bundle sample inputs or basis sets. +Download or import a basis before running a calculation; see +[make basis sets available](installation.md#make-basis-sets-available) and the +[first calculation](first-calculation.md). For development shells, see +[Nix setup](../development/nix-setup.md). diff --git a/docs/src/getting-started/installation.md b/docs/src/getting-started/installation.md index 380ac32f..4da57ce8 100644 --- a/docs/src/getting-started/installation.md +++ b/docs/src/getting-started/installation.md @@ -1,5 +1,10 @@ # Installation +## Install with Nix + +See [installing with Nix](installation-nix.md) for the supported platforms, +profile installation, and CPU-specific builds. + ## Install a prebuilt release binary When a release is available, ordinary users can download the archive for their diff --git a/docs/src/getting-started/rustiq.nix b/docs/src/getting-started/rustiq.nix new file mode 100644 index 00000000..d02128da --- /dev/null +++ b/docs/src/getting-started/rustiq.nix @@ -0,0 +1,7 @@ +# NixOS module: provide the RustiQ flake through specialArgs.inputs. +{ pkgs, inputs, ... }: +{ + environment.systemPackages = [ + (inputs.rustiq.legacyPackages.${pkgs.stdenv.hostPlatform.system}.cpuTarget "native") + ]; +} diff --git a/flake.nix b/flake.nix index fa8224a6..18d03ed4 100644 --- a/flake.nix +++ b/flake.nix @@ -157,6 +157,13 @@ src = cargoSource; strictDeps = true; cargoExtraArgs = "--locked --workspace --no-default-features --features online"; + # Crane builds from a filtered source tree without .git. Supply + # the same flake provenance used by the installed package so + # vergen-gix does not warn or emit placeholder Git metadata. + VERGEN_GIT_SHA = pkgs.lib.removeSuffix "-dirty" ( + inputs.self.rev or inputs.self.dirtyRev or "unknown" + ); + VERGEN_GIT_DIRTY = if inputs.self ? rev then "false" else "true"; }; cargoArtifacts = craneLib.buildDepsOnly commonCargoArgs; @@ -442,6 +449,17 @@ pyscf-environment = pythonPyscf; }; + legacyPackages.cpuTarget = + cpu: + let + cpuFlags = "-C target-cpu=${cpu}"; + cpuArtifacts = craneLib.buildDepsOnly (commonCargoArgs // { RUSTFLAGS = cpuFlags; }); + in + rustiq.overrideAttrs (_: { + RUSTFLAGS = cpuFlags; + cargoArtifacts = cpuArtifacts; + }); + apps = { pyscf-check = { type = "app"; From 35ec44ff15f61e9db77a24bf210aaea45e2ee58b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20V=C3=A9ril?= Date: Thu, 12 Nov 2026 11:38:29 +0100 Subject: [PATCH 07/12] Improve Cargo package metadata and lints (#212) --- .github/workflows/scientific-report.yml | 4 +-- Cargo.toml | 36 ++++++++++++++------- THIRD-PARTY-NOTICES.sha256 | 2 +- THIRD-PARTY-NOTICES.txt | 2 +- crates/rustiq-core/Cargo.toml | 17 ++++++---- crates/rustiq-core/README.md | 21 ++++++++++++ crates/rustiq-core/src/basis/basis_store.rs | 4 +-- crates/rustiq-core/src/eri.rs | 32 +++++++++--------- crates/rustiq-core/src/lib.rs | 2 +- crates/rustiq-core/src/mp2.rs | 28 ++++++++-------- docs/src/development/contributing.md | 18 +++++------ src/cli/build_info/metadata.rs | 4 +-- tests/version_cli.rs | 7 ++-- 13 files changed, 106 insertions(+), 71 deletions(-) create mode 100644 crates/rustiq-core/README.md diff --git a/.github/workflows/scientific-report.yml b/.github/workflows/scientific-report.yml index f006bc37..ab7a1920 100644 --- a/.github/workflows/scientific-report.yml +++ b/.github/workflows/scientific-report.yml @@ -67,12 +67,12 @@ jobs: - name: Run selected ERI Criterion benchmarks id: eri continue-on-error: true - run: cargo bench --features bench-support --bench eri_timings + run: cargo bench --features bench --bench eri_timings - name: Run selected MP2 Criterion benchmarks id: mp2 continue-on-error: true - run: cargo bench --features bench-support --bench mp2_timings + run: cargo bench --features bench --bench mp2_timings - name: Generate performance report id: performance diff --git a/Cargo.toml b/Cargo.toml index 000dbf59..44d0bf86 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,6 +3,14 @@ members = ["crates/rustiq-core"] default-members = [".", "crates/rustiq-core"] resolver = "2" +[workspace.package] +authors = ["Mickaël Véril"] +edition = "2021" +license = "MIT OR Apache-2.0" +repository = "https://github.com/mveril/RustiQ" +homepage = "https://mveril.github.io/RustiQ/" +keywords = ["quantum-chemistry", "electronic-structure", "hartree-fock", "mp2", "chemistry"] + [workspace.lints.rust] unsafe_code = "deny" @@ -10,6 +18,7 @@ unsafe_code = "deny" # Individual lint settings and local, documented exceptions override these groups. all = { level = "deny", priority = -1 } pedantic = { level = "deny", priority = -1 } +cargo = { level = "deny", priority = -1 } unwrap_used = "deny" panic = "deny" panic_in_result_fn = "deny" @@ -56,14 +65,17 @@ tokio = { version = "1", default-features = false } [package] name = "rustiq-chem" -description = "The modern rust quantum chemistry software" -authors = ["Mickaël Véril"] +description = "A modern command-line application for quantum chemistry, written in Rust" +authors.workspace = true version = "0.1.0-alpha.1.dev" -edition = "2021" -license = "MIT OR Apache-2.0" -repository = "https://github.com/mveril/RustiQ" - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +edition.workspace = true +license.workspace = true +repository.workspace = true +homepage.workspace = true +documentation = "https://mveril.github.io/RustiQ/" +readme = "README.md" +keywords.workspace = true +categories = ["science::computational-chemistry::electronic-structure", "command-line-utilities"] [[bin]] name = "rustiq" @@ -72,7 +84,7 @@ path = "src/main.rs" [features] default = ["online", "manpages", "completions"] online = ["rustiq-core/online", "dep:indicatif", "dep:tokio"] -bench-support = ["rustiq-core/bench-support"] +bench = ["rustiq-core/bench"] manpages = ["dep:clap_mangen"] completions = ["dep:clap_complete", "dep:clap_complete_nushell", "dep:sysinfo"] @@ -123,22 +135,22 @@ temp-env.workspace = true [[bench]] name = "mp2_timings" harness = false -required-features = ["bench-support"] +required-features = ["bench"] [[bench]] name = "eri_timings" harness = false -required-features = ["bench-support"] +required-features = ["bench"] [[bench]] name = "eri_storage_allocation" harness = false -required-features = ["bench-support"] +required-features = ["bench"] [[bench]] name = "eri_storage_memory" harness = false -required-features = ["bench-support"] +required-features = ["bench"] [build-dependencies] bat = { workspace = true, features = ["build-assets"] } diff --git a/THIRD-PARTY-NOTICES.sha256 b/THIRD-PARTY-NOTICES.sha256 index 77e3187e..4dd906b4 100644 --- a/THIRD-PARTY-NOTICES.sha256 +++ b/THIRD-PARTY-NOTICES.sha256 @@ -1 +1 @@ -1019e261ed817234736bc7b4dd0aacb41ffcbbc1c89e66bd974592b651dec752 THIRD-PARTY-NOTICES.txt +fd10896a1c4a5a5be0dfdcabb49197b9fb5dc80a93d9e0e8a917ff746ca3169e THIRD-PARTY-NOTICES.txt diff --git a/THIRD-PARTY-NOTICES.txt b/THIRD-PARTY-NOTICES.txt index c652b315..37b26622 100644 --- a/THIRD-PARTY-NOTICES.txt +++ b/THIRD-PARTY-NOTICES.txt @@ -1,5 +1,5 @@ Cargo.lock SHA-256: 7f274ca9e7f738d58ec0e46d03be9802569df0512ec91892f4ccd1edbd90ad02 -Notice inputs SHA-256: 8c276309486abfbe214c47d6d52b2f5b63cf45bb0ecdf95bb6a8610d04ff929f +Notice inputs SHA-256: f3b7a59fa3633c0574f9c0f0b1c6a0b767056d57307cba53913adf72ef06f236 Corresponding source: https://github.com/mveril/RustiQ/releases/download/v0.1.0-alpha.1.dev/rustiq-corresponding-source.tar.gz RustiQ third-party licenses diff --git a/crates/rustiq-core/Cargo.toml b/crates/rustiq-core/Cargo.toml index 9c9ccfdd..48482916 100644 --- a/crates/rustiq-core/Cargo.toml +++ b/crates/rustiq-core/Cargo.toml @@ -1,11 +1,16 @@ [package] name = "rustiq-chem-core" -description = "Reusable domain model and quantum chemistry implementation for RustiQ" -authors = ["Mickaël Véril"] +description = "A reusable Rust library for modern quantum chemistry software" +authors.workspace = true version = "0.1.0-alpha.1.dev" -edition = "2021" -license = "MIT OR Apache-2.0" -repository = "https://github.com/mveril/RustiQ" +edition.workspace = true +license.workspace = true +repository.workspace = true +homepage.workspace = true +documentation = "https://docs.rs/rustiq-chem-core" +readme = "README.md" +keywords.workspace = true +categories = ["science::computational-chemistry::electronic-structure"] # Preserve the Rust import name independently of the Cargo package name. [lib] @@ -20,7 +25,7 @@ workspace = true [features] default = [] online = ["dep:reqwest", "dep:tokio"] -bench-support = [] +bench = [] [dependencies] miette.workspace = true diff --git a/crates/rustiq-core/README.md b/crates/rustiq-core/README.md new file mode 100644 index 00000000..5cdcee41 --- /dev/null +++ b/crates/rustiq-core/README.md @@ -0,0 +1,21 @@ +# RustiQ Core + +RustiQ Core is the reusable scientific library behind [RustiQ](https://github.com/mveril/RustiQ). +It explores how Rust's strong typing, memory safety, and modularity can support +quantum chemistry software, alongside numerical validation and performance. + +The library provides molecular and basis data, scientific configuration, +calculation execution, and persistence. Runfile parsing and terminal output +belong to the RustiQ application. The Cargo package is `rustiq-chem-core`; +its Rust import name is `rustiq_core`. + +RustiQ is experimental. Independently validate results against established +quantum chemistry packages before using them for research conclusions. + +- [API documentation](https://docs.rs/rustiq-chem-core) +- [RustiQ manual](https://mveril.github.io/RustiQ/) +- [Source, examples, and scientific validation](https://github.com/mveril/RustiQ) + +RustiQ-authored source is licensed under MIT OR Apache-2.0. See the +[distribution terms](https://github.com/mveril/RustiQ/blob/main/DISTRIBUTION.md) +for third-party licensing information. diff --git a/crates/rustiq-core/src/basis/basis_store.rs b/crates/rustiq-core/src/basis/basis_store.rs index d13722f4..6439fa78 100644 --- a/crates/rustiq-core/src/basis/basis_store.rs +++ b/crates/rustiq-core/src/basis/basis_store.rs @@ -154,7 +154,7 @@ impl BasisStore { } /// Copies a basis file from another store into this store. - #[cfg(any(test, feature = "bench-support"))] + #[cfg(any(test, feature = "bench"))] #[allow( dead_code, reason = "Retained scientific helpers and representations support tests, benchmarks, or future internal use" @@ -174,7 +174,7 @@ impl BasisStore { } /// Returns the package-local fixture basis store used by tests and benches. - #[cfg(any(test, feature = "bench-support"))] + #[cfg(any(test, feature = "bench"))] #[allow( dead_code, reason = "Retained scientific helpers and representations support tests, benchmarks, or future internal use" diff --git a/crates/rustiq-core/src/eri.rs b/crates/rustiq-core/src/eri.rs index 06531264..464b47c1 100644 --- a/crates/rustiq-core/src/eri.rs +++ b/crates/rustiq-core/src/eri.rs @@ -7,9 +7,9 @@ #[cfg(test)] use mendeleev::Element; use std::f64::consts::PI; -#[cfg(feature = "bench-support")] +#[cfg(feature = "bench")] use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering}; -#[cfg(feature = "bench-support")] +#[cfg(feature = "bench")] use std::time::{Duration, Instant}; mod compact; pub use compact::CompactEri; @@ -27,16 +27,16 @@ use thiserror::Error; const ERI_SELF_INTEGRAL_NEGATIVE_TOLERANCE: f64 = 1e-12; const COULOMB_CACHE_SMALLVEC_CAPACITY: usize = 128; -#[cfg(feature = "bench-support")] +#[cfg(feature = "bench")] const CACHE_SIZE_BUCKET_LIMITS: [usize; 8] = [16, 32, 64, 128, 256, 512, 1024, usize::MAX]; -#[cfg(feature = "bench-support")] +#[cfg(feature = "bench")] static COULOMB_CACHE_SIZE_COUNT: AtomicU64 = AtomicU64::new(0); -#[cfg(feature = "bench-support")] +#[cfg(feature = "bench")] static COULOMB_CACHE_SIZE_SUM: AtomicU64 = AtomicU64::new(0); -#[cfg(feature = "bench-support")] +#[cfg(feature = "bench")] static COULOMB_CACHE_SIZE_MAX: AtomicUsize = AtomicUsize::new(0); -#[cfg(feature = "bench-support")] +#[cfg(feature = "bench")] static COULOMB_CACHE_SIZE_BUCKETS: [AtomicU64; 8] = [ AtomicU64::new(0), AtomicU64::new(0), @@ -220,7 +220,7 @@ pub(crate) fn electron_repulsion_ints_with_threshold( )) } -#[cfg(feature = "bench-support")] +#[cfg(feature = "bench")] #[allow( dead_code, reason = "Retained scientific helpers and representations support tests, benchmarks, or future internal use" @@ -238,7 +238,7 @@ pub struct EriTimingBreakdown { pub coulomb_cache_sizes: CacheSizeStats, } -#[cfg(feature = "bench-support")] +#[cfg(feature = "bench")] #[allow( dead_code, reason = "Retained scientific helpers and representations support tests, benchmarks, or future internal use" @@ -252,7 +252,7 @@ pub struct CacheSizeStats { pub buckets: [(usize, u64); 8], } -#[cfg(feature = "bench-support")] +#[cfg(feature = "bench")] #[allow( dead_code, reason = "Retained scientific helpers and representations support tests, benchmarks, or future internal use" @@ -281,7 +281,7 @@ impl CacheSizeStats { } } -#[cfg(feature = "bench-support")] +#[cfg(feature = "bench")] #[allow( dead_code, reason = "Retained scientific helpers and representations support tests, benchmarks, or future internal use" @@ -292,7 +292,7 @@ pub fn electron_repulsion_ints_timed( electron_repulsion_ints_timed_with_observer(basis, |_, _| {}) } -#[cfg(feature = "bench-support")] +#[cfg(feature = "bench")] pub fn electron_repulsion_ints_timed_with_observer( basis: &Basis, mut observer: impl FnMut(&'static str, Duration), @@ -498,7 +498,7 @@ fn compute_eri_pair_primitive(primitive_ab: &PairPrimitive, primitive_cd: &PairP 2.0 * PI.powf(2.5) / (p * q * (p + q).sqrt()) * eri } -#[cfg(feature = "bench-support")] +#[cfg(feature = "bench")] fn reset_coulomb_cache_size_stats() { COULOMB_CACHE_SIZE_COUNT.store(0, Ordering::Relaxed); COULOMB_CACHE_SIZE_SUM.store(0, Ordering::Relaxed); @@ -508,7 +508,7 @@ fn reset_coulomb_cache_size_stats() { } } -#[cfg(feature = "bench-support")] +#[cfg(feature = "bench")] fn record_coulomb_cache_size(len: usize) { COULOMB_CACHE_SIZE_COUNT.fetch_add(1, Ordering::Relaxed); COULOMB_CACHE_SIZE_SUM.fetch_add(len as u64, Ordering::Relaxed); @@ -521,7 +521,7 @@ fn record_coulomb_cache_size(len: usize) { COULOMB_CACHE_SIZE_BUCKETS[bucket_index].fetch_add(1, Ordering::Relaxed); } -#[cfg(feature = "bench-support")] +#[cfg(feature = "bench")] fn coulomb_cache_size_stats() -> CacheSizeStats { let mut buckets = [(0, 0); 8]; for (index, limit) in CACHE_SIZE_BUCKET_LIMITS.iter().copied().enumerate() { @@ -648,7 +648,7 @@ impl CoulombAuxiliaryCache { * (u_max as usize + 1) * (v_max as usize + 1) * (n_max as usize + 1); - #[cfg(feature = "bench-support")] + #[cfg(feature = "bench")] record_coulomb_cache_size(len); let mut values = SmallVec::new(); values.resize(len, f64::NAN); diff --git a/crates/rustiq-core/src/lib.rs b/crates/rustiq-core/src/lib.rs index a0e54583..f75b79be 100644 --- a/crates/rustiq-core/src/lib.rs +++ b/crates/rustiq-core/src/lib.rs @@ -89,7 +89,7 @@ pub(crate) mod resources; #[deny(clippy::doc_markdown)] pub(crate) mod test_utils; -#[cfg(feature = "bench-support")] +#[cfg(feature = "bench")] #[deny(clippy::doc_markdown)] pub mod bench_support { pub use crate::basis::BasisStore; diff --git a/crates/rustiq-core/src/mp2.rs b/crates/rustiq-core/src/mp2.rs index 96cc95bd..4c5bfec5 100644 --- a/crates/rustiq-core/src/mp2.rs +++ b/crates/rustiq-core/src/mp2.rs @@ -19,11 +19,11 @@ //! rejected to prevent numerically singular perturbative contributions. mod blocked; -#[cfg(any(test, feature = "bench-support"))] +#[cfg(any(test, feature = "bench"))] use crate::eri::index::{EriIndex, PairIndex}; pub use blocked::{Mp2MemoryPlan, Mp2Sector}; use nalgebra::{DMatrix, DVector}; -#[cfg(any(test, feature = "bench-support"))] +#[cfg(any(test, feature = "bench"))] use rayon::prelude::*; use thiserror::Error; @@ -263,7 +263,7 @@ pub(crate) fn correlation_energy_with_memory( ) } -#[cfg(any(test, feature = "bench-support"))] +#[cfg(any(test, feature = "bench"))] fn build_ovov_integrals(input: &Mp2Input<'_>) -> DMatrix { let mo_coefficients = input.mo_coefficients; let basis_functions = mo_coefficients.nrows(); @@ -366,7 +366,7 @@ pub(crate) fn uhf_correlation_energy_with_memory( Ok(result) } -#[cfg(any(test, feature = "bench-support"))] +#[cfg(any(test, feature = "bench"))] fn build_ao_pair_matrix( two_electron_integrals: &CompactEri, basis_functions: usize, @@ -391,7 +391,7 @@ fn build_ao_pair_matrix( matrix } -#[cfg(any(test, feature = "bench-support"))] +#[cfg(any(test, feature = "bench"))] fn build_orbital_pair_transform( mo_coefficients: &DMatrix, occupied_start: usize, @@ -446,7 +446,7 @@ fn validate_spin_input(input: &Mp2SpinInput<'_>) -> Result<(), Mp2Error> { Ok(()) } -#[cfg(any(test, feature = "bench-support"))] +#[cfg(any(test, feature = "bench"))] fn same_spin_correlation_energy( ovov_integrals: &DMatrix, orbital_energies: &DVector, @@ -496,7 +496,7 @@ fn same_spin_correlation_energy( Ok(correlation_energy) } -#[cfg(any(test, feature = "bench-support"))] +#[cfg(any(test, feature = "bench"))] fn opposite_spin_correlation_energy( ovov_integrals: &DMatrix, alpha_orbital_energies: &DVector, @@ -553,7 +553,7 @@ pub(crate) struct Mp2SpinInput<'a> { pub(crate) frozen_orbitals: usize, } -#[cfg(any(test, feature = "bench-support"))] +#[cfg(any(test, feature = "bench"))] fn pair_transform_coefficient( mo_coefficients: &DMatrix, mu: usize, @@ -569,12 +569,12 @@ fn pair_transform_coefficient( } } -#[cfg(any(test, feature = "bench-support"))] +#[cfg(any(test, feature = "bench"))] fn orbital_pair_index(left: usize, right: usize, right_count: usize) -> usize { left * right_count + right } -#[cfg(any(test, feature = "bench-support"))] +#[cfg(any(test, feature = "bench"))] fn basis_function_pair_count(basis_functions: usize) -> usize { basis_functions * (basis_functions + 1) / 2 } @@ -614,7 +614,7 @@ fn mo_two_electron_integral( value } -#[cfg(feature = "bench-support")] +#[cfg(feature = "bench")] #[non_exhaustive] pub struct Mp2BenchResult { pub dense_elapsed: std::time::Duration, @@ -624,7 +624,7 @@ pub struct Mp2BenchResult { pub memory: Mp2MemoryPlan, } -#[cfg(feature = "bench-support")] +#[cfg(feature = "bench")] pub struct Mp2BenchInput { coefficients: DMatrix, energies: DVector, @@ -632,7 +632,7 @@ pub struct Mp2BenchInput { occupied_orbitals: usize, } -#[cfg(feature = "bench-support")] +#[cfg(feature = "bench")] impl Mp2BenchInput { /// Creates deterministic synthetic input for comparing MP2 implementations. #[must_use] @@ -735,7 +735,7 @@ impl Mp2BenchInput { } /// Synthetic, deterministic inputs; timing excludes input/ERI construction and SCF. -#[cfg(feature = "bench-support")] +#[cfg(feature = "bench")] /// /// # Errors /// diff --git a/docs/src/development/contributing.md b/docs/src/development/contributing.md index 83976b37..f5ce85e9 100644 --- a/docs/src/development/contributing.md +++ b/docs/src/development/contributing.md @@ -360,13 +360,13 @@ Run `cargo test -p rustiq-chem-core math_utils::boys` and the affected [PySCF comparisons](https://github.com/mveril/RustiQ/blob/main/tools/reference/README.md) after changing this path. The `eri_timings` and `mp2_timings` benchmarks use Criterion and require -`bench-support`: +`bench`: ```sh -cargo bench --bench mp2_timings --features bench-support -cargo bench --bench eri_timings --features bench-support -cargo bench --bench eri_storage_allocation --features bench-support -cargo bench --bench eri_storage_memory --features bench-support +cargo bench --bench mp2_timings --features bench +cargo bench --bench eri_timings --features bench +cargo bench --bench eri_storage_allocation --features bench +cargo bench --bench eri_storage_memory --features bench ``` The MP2 benchmark compares the dense AO-to-MO transformation and correlation @@ -431,8 +431,8 @@ baseline on the reference revision with the same workload, then compare from the revision under test: ```sh -cargo bench --bench mp2_timings --features bench-support -- --save-baseline before -cargo bench --bench mp2_timings --features bench-support -- --baseline before +cargo bench --bench mp2_timings --features bench -- --save-baseline before +cargo bench --bench mp2_timings --features bench -- --baseline before ``` See the [MP2 benchmark source](https://github.com/mveril/RustiQ/blob/main/benches/mp2_timings.rs) @@ -461,8 +461,8 @@ platform metadata so results can be compared across revisions. Generate Criterion measurements and their report with: ```sh -cargo bench --features bench-support --bench eri_timings -cargo bench --features bench-support --bench mp2_timings +cargo bench --features bench --bench eri_timings +cargo bench --features bench --bench mp2_timings uv run --locked python tools/benchmarks/report.py ``` diff --git a/src/cli/build_info/metadata.rs b/src/cli/build_info/metadata.rs index cc7afffa..135d7aa7 100644 --- a/src/cli/build_info/metadata.rs +++ b/src/cli/build_info/metadata.rs @@ -169,8 +169,8 @@ mod tests { #[test] fn cargo_feature_list_hides_the_default_marker_and_is_sorted() { assert_eq!( - cargo_feature_values("online,default,bench_support,online"), - ["bench-support", "online"] + cargo_feature_values("online,default,bench,online"), + ["bench", "online"] ); assert_eq!(cargo_feature_values("default"), Vec::::new()); } diff --git a/tests/version_cli.rs b/tests/version_cli.rs index 5f9ec90c..c5c6f1b1 100644 --- a/tests/version_cli.rs +++ b/tests/version_cli.rs @@ -128,15 +128,12 @@ fn verbose_version_accepts_both_orders_and_short_flags() { let feature_line = lines .iter() .find(|line| line.starts_with(" Cargo features")); - let expected_any_feature = cfg!(feature = "online") || cfg!(feature = "bench-support"); + let expected_any_feature = cfg!(feature = "online") || cfg!(feature = "bench"); assert_eq!(feature_line.is_some(), expected_any_feature); if let Some(feature_line) = feature_line { assert!(!feature_line.contains("default")); assert_eq!(feature_line.contains("online"), cfg!(feature = "online")); - assert_eq!( - feature_line.contains("bench-support"), - cfg!(feature = "bench-support") - ); + assert_eq!(feature_line.contains("bench"), cfg!(feature = "bench")); } if let Some(debug_line) = lines .iter() From bee87c9fb01c5ecaf44991f7626d1493bf0f2838 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20V=C3=A9ril?= Date: Thu, 12 Nov 2026 14:01:04 +0100 Subject: [PATCH 08/12] Migrate PySCF reference checks to 2.15 (#214) --- .github/workflows/ci.yml | 21 ++++++++++ docs/src/development/manual-setup.md | 17 +++----- docs/src/development/platforms.md | 10 +++-- docs/src/user-guide/hartree-fock.md | 2 +- docs/src/user-guide/json-output.md | 2 +- docs/src/user-guide/mp2.md | 2 +- pyproject.toml | 14 +++++-- tools/reference/README.md | 10 ++--- tools/reference/integrals.py | 2 +- tools/reference/test_integrals.py | 9 ++++- uv.lock | 60 ++++++++++++++++++++++------ 11 files changed, 106 insertions(+), 43 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7e72e61b..61866dc8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -128,6 +128,27 @@ jobs: if: matrix.os == 'windows-latest' && matrix.python-version == '3.12' run: uv run --locked --python ${{ matrix.python-version }} pytest tools/reference/test_persistence_interop.py -k reads_rust + pyscf-native: + name: PySCF reference suite (${{ matrix.os }}) + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, ubuntu-24.04-arm, macos-15-intel, macos-latest, windows-latest] + steps: + - name: Checkout + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - name: Set up uv + uses: astral-sh/setup-uv@68559e99cc807c1eb7e9238fd43a15bba460eec2 # v10.3.3 + with: + python-version: '3.12' + - name: Install Rust stable + uses: dtolnay/rust-toolchain@4360b52568e2003a75bf9bc1d59f33a8e3fc893c # stable + - name: Cache Rust builds + uses: Swatinem/rust-cache@d1527c8f67ef9af357290475d4af4b0fab8f12e0 # v2.9.3 + - name: Run PySCF comparisons + run: uv run --locked pytest tools/reference + nix: name: Nix flake (${{ matrix.system }}) runs-on: ${{ matrix.os }} diff --git a/docs/src/development/manual-setup.md b/docs/src/development/manual-setup.md index 0575da67..d0ec5b66 100644 --- a/docs/src/development/manual-setup.md +++ b/docs/src/development/manual-setup.md @@ -10,8 +10,7 @@ dependencies through `Cargo.lock`, but does not pin the entire host environment. - **Platforms:** native Cargo supports the platforms in the [compatibility table](platforms.md), including Windows and Intel macOS. - Python development tools run natively on Windows; PySCF comparisons currently - require Linux, macOS, or WSL2. + Python development tools run natively on Windows; PySCF comparisons also run on Windows x86-64. - **HPC use:** this route can use the site's existing compiler environment without Docker or Nix. Build and run according to cluster policy, using the scheduler's assigned resources. A locally built binary still needs a @@ -52,21 +51,17 @@ uv run --locked pytest tools/test_book_links.py tools/test_release_smoke.py tool The project supports Python 3.12 through 3.14. The default groups install JupyterLab, ipykernel, Matplotlib, SciPy, NumPy, pytest, and Ruff. The lock -also includes PySCF 2.14.0 on Linux and macOS, but that pinned release has no -Windows wheel, so uv excludes it on native Windows. Binary wheels are required; +also includes PySCF 2.15.0 on Linux, macOS, and Windows x86-64. +Windows ARM64 remains excluded. Binary wheels are required; source builds remain disabled. Run the reference suite with: ```sh uv run --locked pytest tools/reference ``` -On native Windows, only the PySCF calculation tests in -`tools/reference/test_compare_pyscf.py` and `tools/reference/test_integrals.py` -are skipped. Helper tests in -`test_compare_pyscf_helpers.py`, report validation, and NumPy persistence tests, -including Rust-generated persistence artifacts, remain available. Use WSL2 -for PySCF comparisons and the scientific report -generator (`tools/reference/report.py`). These Python tools support +On Windows x86-64, the same command runs PySCF calculations, AO integral +comparisons, helper tests, and persistence interoperability tests. Use WSL2 +for PySCF on Windows ARM64. These Python tools support development and validation; building the RustiQ executable still requires the Rust toolchain and the Cargo commands above. diff --git a/docs/src/development/platforms.md b/docs/src/development/platforms.md index 5c658c41..25484ab4 100644 --- a/docs/src/development/platforms.md +++ b/docs/src/development/platforms.md @@ -42,9 +42,9 @@ Windows troubleshooting, and the configured VS Code extensions. The optional scientific Python tools have their own platform requirements. The project configures `uv` for Linux, macOS, and native Windows. The locked -PySCF release is excluded on native Windows; the remaining Python tools and -tests run there. The Nix environment provides PySCF on every declared Nix -system. For PySCF comparisons on Windows, use WSL2 or the Linux Dev Container. +PySCF 2.15.0 release includes native Windows x86-64 wheels; Windows ARM64 +remains excluded. The Nix environment provides PySCF on every declared Nix +system. For PySCF comparisons on Windows ARM64, use WSL2 or the Linux Dev Container. ## How compatibility is checked @@ -61,7 +61,9 @@ provides ongoing evidence for this support: and PySCF comparisons. Separate Windows and macOS host integrations are not exercised by these checks. - The standalone `uv` environment is checked on Linux and Windows x86_64 with - Python 3.12 and 3.14. Windows skips the PySCF comparison modules and also checks + Python 3.12 and 3.14. A native PySCF job runs the reference suite on Linux + x86-64 and ARM64, macOS Intel and Apple Silicon, and Windows x86-64. + Windows also checks Rust/NumPy persistence interoperability with Python 3.12. The [scheduled full Nix workflow](https://github.com/mveril/RustiQ/actions/workflows/nix-full.yml) diff --git a/docs/src/user-guide/hartree-fock.md b/docs/src/user-guide/hartree-fock.md index 115c3981..2ebcd542 100644 --- a/docs/src/user-guide/hartree-fock.md +++ b/docs/src/user-guide/hartree-fock.md @@ -165,7 +165,7 @@ The electronic HF energy includes electronic kinetic, electron–nuclear, and electron–electron terms. Total HF energy adds nuclear repulsion. Energies are in Hartree; input Angstrom coordinates are converted to Bohr internally. Terminal energies are rounded for display. The H₂/STO-3G example is checked -against a PySCF 2.14.0 reference in the project comparison suite with an +against a PySCF 2.15.0 reference in the project comparison suite with an absolute energy tolerance of \\(2\times10^{-10}\\) Hartree; the CLI test checks the rounded value. See [reference comparison coverage](../scientific-scope.md#reference-comparisons) and the [output guide](../getting-started/output.md). diff --git a/docs/src/user-guide/json-output.md b/docs/src/user-guide/json-output.md index 83cadc25..8c5932f9 100644 --- a/docs/src/user-guide/json-output.md +++ b/docs/src/user-guide/json-output.md @@ -107,6 +107,6 @@ for readability. Do not compare a rounded terminal value against a tight numerical tolerance. Existing JSON CLI tests validate both schemas and full-precision H₂ output in [`tests/json_output_cli.rs`](https://github.com/mveril/RustiQ/blob/main/tests/json_output_cli.rs). -The checked-in reference tests compare JSON values against PySCF 2.14.0 for +The checked-in reference tests compare JSON values against PySCF 2.15.0 for selected systems; see [scientific scope](../scientific-scope.md#reference-comparisons). For terminology used in result fields, see the [glossary](../glossary.md). diff --git a/docs/src/user-guide/mp2.md b/docs/src/user-guide/mp2.md index 078e4bc4..f5041ce8 100644 --- a/docs/src/user-guide/mp2.md +++ b/docs/src/user-guide/mp2.md @@ -159,7 +159,7 @@ budget resolution follows the shared [resource policy](https://github.com/mveril The H₂/STO-3G RHF-MP2 comparison checks the HF energy within \\(2\times10^{-10}\\) Hartree and MP2 correlation energy within \\(10^{-10}\\) Hartree against PySCF -2.14.0. The OH/STO-3G UHF-MP2 case checks HF energy within \\(5\times10^{-8}\\), +2.15.0. The OH/STO-3G UHF-MP2 case checks HF energy within \\(5\times10^{-8}\\), correlation energy within \\(5\times10^{-9}\\) Hartree, and spin expectation within \\(2\times10^{-5}\\). These are agreement tolerances for those inputs, not a measure of general chemical accuracy. See the diff --git a/pyproject.toml b/pyproject.toml index 44ed743b..8c2696c1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,13 +3,19 @@ name = "rustiq-reference-tools" version = "0.0.0" description = "Python tooling used to validate RustiQ against reference implementations" requires-python = ">=3.12,<3.15" -# PySCF 2.14.0 predates upstream Windows-wheel publishing; PR #3424 adds Windows wheels to PyPI releases. -# Recheck this marker when upgrading PySCF: https://github.com/pyscf/pyscf/pull/3424 -dependencies = ["pyscf==2.14.0; sys_platform != 'win32'"] +# PySCF publishes native Windows wheels for x86-64; Windows ARM64 still requires WSL2. +dependencies = ["pyscf==2.15.0; sys_platform != 'win32' or platform_machine == 'AMD64' or platform_machine == 'x86_64'"] [dependency-groups] test = ["numpy", "pytest"] -dev = ["ipykernel", "jupyterlab", "matplotlib", "scipy"] +# 26.1.0 has no macOS Intel wheel; remove this constraint when one is published. +dev = [ + "ipykernel", + "jupyterlab", + "matplotlib", + "scipy", + "argon2-cffi-bindings==25.1.0; sys_platform == 'darwin' and platform_machine == 'x86_64'", +] linting = ["ruff"] [tool.uv] diff --git a/tools/reference/README.md b/tools/reference/README.md index 321bf3dc..6d8937d3 100644 --- a/tools/reference/README.md +++ b/tools/reference/README.md @@ -50,10 +50,10 @@ uv run --locked pytest tools/reference This uses the cross-platform lock for Python 3.12 through 3.14 and requires compatible binary wheels; it does not build Python packages from source. PySCF -supports Linux, macOS, and WSL2, but the pinned release has no native Windows -wheel. The lock excludes PySCF on native Windows. Windows uv jobs still run the -PySCF-independent report and helper tests; the PySCF comparison and AO -integral suites run in the Nix PySCF environment or WSL2. Cargo must remain on +2.15.0 provides binary wheels for Linux, macOS, and native Windows x86-64. +Windows ARM64 remains excluded; use WSL2 there. The native PySCF CI job runs +the full reference suite, including energy and AO integral comparisons, on +supported Linux, macOS, and Windows x86-64 runners. Cargo must remain on `PATH` when using the default `cargo run` invocation. Set `RUSTIQ_BIN` to an absolute executable path to compare a specific build instead; `nix run .#pyscf-check` supplies its Nix-built executable this way. @@ -265,7 +265,7 @@ reference, not a correlated MP2 spin expectation. ### Absolute tolerances -Measured errors with pinned PySCF 2.14.0 and single-thread BLAS/OpenMP are below. +Measured errors with PySCF 2.14.0 and single-thread BLAS/OpenMP are below. The tolerances leave a numerical margin over the observed errors for independent SCF termination and platform-dependent floating-point reductions; they do not represent physical error bars. No existing tolerance is increased. diff --git a/tools/reference/integrals.py b/tools/reference/integrals.py index e2f4b658..b9e04975 100644 --- a/tools/reference/integrals.py +++ b/tools/reference/integrals.py @@ -193,7 +193,7 @@ def validate(): env=env, ) except RuntimeError as error: - if not list(output.glob("*.json")): + if any(not (output / f"{name}.json").exists() for name, *_ in CASES): raise cargo_error = str(error) rust = [json.loads((output / f"{name}.json").read_text()) for name, *_ in CASES] diff --git a/tools/reference/test_integrals.py b/tools/reference/test_integrals.py index 1517b1a9..028de67c 100644 --- a/tools/reference/test_integrals.py +++ b/tools/reference/test_integrals.py @@ -6,7 +6,7 @@ import numpy as np import pytest -# PySCF is intentionally unavailable on Windows for the pinned reference suite. +# PySCF wheels are unavailable on some architectures, including Windows ARM64. # This module contains only tests that execute PySCF. pytest.importorskip("pyscf", reason="AO integral references require PySCF") @@ -24,8 +24,13 @@ def test_ao_integrals(): def test_checked_in_pyscf_references(): metadata, generated = generate_fixture() stored = json.loads(FIXTURE.read_text()) + # The checked-in arrays retain the PySCF version that generated them. + # Compare their values with the currently pinned PySCF independently. + metadata.pop("pyscf_version") assert metadata == { - key: value for key, value in stored.items() if key != "array_sha256" + key: value + for key, value in stored.items() + if key not in {"array_sha256", "pyscf_version"} } assert set(stored["array_sha256"]) == set(generated) for name, values in generated.items(): diff --git a/uv.lock b/uv.lock index 9c8d3e12..f474d291 100644 --- a/uv.lock +++ b/uv.lock @@ -3,10 +3,10 @@ revision = 5 requires-python = ">=3.12, <3.15" resolution-markers = [ "python_full_version >= '3.14' and sys_platform == 'darwin'", - "python_full_version < '3.14' and sys_platform == 'darwin'", "python_full_version >= '3.14' and sys_platform == 'linux'", - "python_full_version < '3.14' and sys_platform == 'linux'", "python_full_version >= '3.14' and sys_platform == 'win32'", + "python_full_version < '3.14' and sys_platform == 'darwin'", + "python_full_version < '3.14' and sys_platform == 'linux'", "python_full_version < '3.14' and sys_platform == 'win32'", ] supported-markers = [ @@ -42,23 +42,50 @@ name = "argon2-cffi" version = "25.1.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "argon2-cffi-bindings" }, + { name = "argon2-cffi-bindings", version = "25.1.0", source = { registry = "https://pypi.org/simple" }, marker = "sys_platform != 'linux' and sys_platform != 'win32'" }, + { name = "argon2-cffi-bindings", version = "26.1.0", source = { registry = "https://pypi.org/simple" }, marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/0e/89/ce5af8a7d472a67cc819d5d998aa8c82c5d860608c4db9f46f1162d7dab9/argon2_cffi-25.1.0.tar.gz", hash = "sha256:694ae5cc8a42f4c4e2bf2ca0e64e51e23a040c6a517a85074683d3959e1346c1", size = 45706, upload-time = "2025-06-03T06:55:32.073Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/4f/d3/a8b22fa575b297cd6e3e3b0155c7e25db170edf1c74783d6a31a2490b8d9/argon2_cffi-25.1.0-py3-none-any.whl", hash = "sha256:fdc8b074db390fccb6eb4a3604ae7231f219aa669a2652e0f20e16ba513d5741", size = 14657, upload-time = "2025-06-03T06:55:30.804Z" }, ] +[[package]] +name = "argon2-cffi-bindings" +version = "25.1.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14' and sys_platform == 'darwin'", + "python_full_version < '3.14' and sys_platform == 'darwin'", +] +dependencies = [ + { name = "cffi" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5c/2d/db8af0df73c1cf454f71b2bbe5e356b8c1f8041c979f505b3d3186e520a9/argon2_cffi_bindings-25.1.0.tar.gz", hash = "sha256:b957f3e6ea4d55d820e40ff76f450952807013d361a65d7f28acc0acbf29229d", size = 1783441, upload-time = "2025-07-30T10:02:05.147Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/60/97/3c0a35f46e52108d4707c44b95cfe2afcafc50800b5450c197454569b776/argon2_cffi_bindings-25.1.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:3d3f05610594151994ca9ccb3c771115bdb4daef161976a266f0dd8aa9996b8f", size = 54393, upload-time = "2025-07-30T10:01:40.97Z" }, + { url = "https://files.pythonhosted.org/packages/9d/f4/98bbd6ee89febd4f212696f13c03ca302b8552e7dbf9c8efa11ea4a388c3/argon2_cffi_bindings-25.1.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:8b8efee945193e667a396cbc7b4fb7d357297d6234d30a489905d96caabde56b", size = 29328, upload-time = "2025-07-30T10:01:41.916Z" }, + { url = "https://files.pythonhosted.org/packages/43/24/90a01c0ef12ac91a6be05969f29944643bc1e5e461155ae6559befa8f00b/argon2_cffi_bindings-25.1.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:3c6702abc36bf3ccba3f802b799505def420a1b7039862014a65db3205967f5a", size = 31269, upload-time = "2025-07-30T10:01:42.716Z" }, + { url = "https://files.pythonhosted.org/packages/1d/57/96b8b9f93166147826da5f90376e784a10582dd39a393c99bb62cfcf52f0/argon2_cffi_bindings-25.1.0-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:aecba1723ae35330a008418a91ea6cfcedf6d31e5fbaa056a166462ff066d500", size = 54121, upload-time = "2025-07-30T10:01:50.815Z" }, + { url = "https://files.pythonhosted.org/packages/0a/08/a9bebdb2e0e602dde230bdde8021b29f71f7841bd54801bcfd514acb5dcf/argon2_cffi_bindings-25.1.0-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:2630b6240b495dfab90aebe159ff784d08ea999aa4b0d17efa734055a07d2f44", size = 29177, upload-time = "2025-07-30T10:01:51.681Z" }, + { url = "https://files.pythonhosted.org/packages/b6/02/d297943bcacf05e4f2a94ab6f462831dc20158614e5d067c35d4e63b9acb/argon2_cffi_bindings-25.1.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:7aef0c91e2c0fbca6fc68e7555aa60ef7008a739cbe045541e438373bc54d2b0", size = 31090, upload-time = "2025-07-30T10:01:53.184Z" }, +] + [[package]] name = "argon2-cffi-bindings" version = "26.1.0" source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14' and sys_platform == 'linux'", + "python_full_version >= '3.14' and sys_platform == 'win32'", + "python_full_version < '3.14' and sys_platform == 'linux'", + "python_full_version < '3.14' and sys_platform == 'win32'", +] dependencies = [ { name = "cffi" }, ] sdist = { url = "https://files.pythonhosted.org/packages/0b/43/bb8b6e8708d49a5ab36781333af092d9f483b198a2710d01281204640055/argon2_cffi_bindings-26.1.0.tar.gz", hash = "sha256:63505c71542a44b68b1e38060450fb006404170da375feb31af153e7f9c6205d", size = 1790807, upload-time = "2026-08-20T07:44:22.492Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e7/d2/0ae991f1b2181e5be49007c574710a800ad36c2978683addb3e67c474e55/argon2_cffi_bindings-26.1.0-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:21ca0396fe5ec995dd54431c32698189666f9224810acfa752e50d2bd94d9df2", size = 25521, upload-time = "2026-08-20T07:32:43.019Z" }, { url = "https://files.pythonhosted.org/packages/7e/e4/ad91d8297638aa2258aad4501c306aca99480dfe76ccd638173fa3702db9/argon2_cffi_bindings-26.1.0-cp310-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:78de2d65e0b9ea7ce9d1b1c3e87297b2d7305a02c266ee2a2d6910daddd7ee69", size = 27177, upload-time = "2026-08-20T07:32:44.158Z" }, { url = "https://files.pythonhosted.org/packages/6f/86/5363df11b86d02cf3662208e7406496327649cc90eb365bf6f4e8a54a41f/argon2_cffi_bindings-26.1.0-cp310-abi3-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:27f1821903e2ceadcb88ec2b45ef190897b7682449c772f4d9b53e42c520cf29", size = 26597, upload-time = "2026-08-20T07:32:45.172Z" }, { url = "https://files.pythonhosted.org/packages/f4/b5/a14dcc592652347dad23ee93b278a4da5d2a25c9ed3ebd10d68eea823a4f/argon2_cffi_bindings-26.1.0-cp310-abi3-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d88e5f7e60f28ae0b0cc6b2f16c43e87cd642a196a86f85e0d8bb6fe016fc16d", size = 27403, upload-time = "2026-08-20T07:32:46.13Z" }, @@ -70,7 +97,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/4e/db/d83cf2af140547f0b9cdaece05b2dc2dcbf991be4667331d073eff771435/argon2_cffi_bindings-26.1.0-cp310-abi3-win_arm64.whl", hash = "sha256:f9c4420a7a864fe1b86ce35befc95b8e39fb852493b81cf798671ddc265de638", size = 24478, upload-time = "2026-08-20T07:32:52.111Z" }, { url = "https://files.pythonhosted.org/packages/bb/5f/f652055e18d2627e2eed94c7f31a792127cfe38df786635395d742321674/argon2_cffi_bindings-26.1.0-cp313-cp313-pyemscripten_2025_0_wasm32.whl", hash = "sha256:af11ac37a7c53dc16cb7950a6190851b0870fe218b6c60c0bb7ac355234e3083", size = 15434, upload-time = "2026-08-20T07:32:53.143Z" }, { url = "https://files.pythonhosted.org/packages/76/38/de696045960f5b846d428c0fb6c130ed3da87aac2af209b05c193815404c/argon2_cffi_bindings-26.1.0-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:db0fcd827ca61622a01b220aadfbece01939acf53888f2cb98cd93e9b1e2c97e", size = 15449, upload-time = "2026-08-20T07:32:54.075Z" }, - { url = "https://files.pythonhosted.org/packages/91/0a/c25af768f6b75a5a71e31207f87c540656b2808c015260444a22763221ad/argon2_cffi_bindings-26.1.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:28524438cd3e723f25412f63d4fd516ff5bae9ae5aa56acbe2a1404398a0cf31", size = 25683, upload-time = "2026-08-20T07:32:55.05Z" }, { url = "https://files.pythonhosted.org/packages/a8/7e/be212c751ab0bcea7f646615f933bf262e8e50b3f7bef32f861d0a2d066b/argon2_cffi_bindings-26.1.0-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ac82fc756a446b6ccd7139ce70efa9d8bbe541e7ad579a12dcb52764b7175c5f", size = 27311, upload-time = "2026-08-20T07:32:56.166Z" }, { url = "https://files.pythonhosted.org/packages/a6/ee/f84b28e4afd13d3cac36c1d8fa8c239d2dc2c51cd978d02ee5d5ad98d9bb/argon2_cffi_bindings-26.1.0-cp314-cp314t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6a4e68eed961a8de6928d1c17ff3dc2a547e0e923c17f8f1cd79fb7bc9502f98", size = 26771, upload-time = "2026-08-20T07:32:57.206Z" }, { url = "https://files.pythonhosted.org/packages/21/c3/95c07a023691ecd529da9cb6a8f0779e13ebc1bdfaa86d145fdc1c6e7e79/argon2_cffi_bindings-26.1.0-cp314-cp314t-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:151dfaad9de753f4af2a7854e707e4784f2acc434340ade64239c5b104b2d605", size = 27568, upload-time = "2026-08-20T07:32:58.361Z" }, @@ -536,24 +562,28 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/9e/e9/1a19e42cd43cc1365e127db6aae85e1c671da1d9a5d746f4d34a50edb577/h5py-3.16.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:dfc21898ff025f1e8e67e194965a95a8d4754f452f83454538f98f8a3fcb207e", size = 5405250, upload-time = "2026-03-06T13:48:09.628Z" }, { url = "https://files.pythonhosted.org/packages/b7/8e/9790c1655eabeb85b92b1ecab7d7e62a2069e53baefd58c98f0909c7a948/h5py-3.16.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:698dd69291272642ffda44a0ecd6cd3bda5faf9621452d255f57ce91487b9794", size = 5190108, upload-time = "2026-03-06T13:48:11.26Z" }, { url = "https://files.pythonhosted.org/packages/51/d7/ab693274f1bd7e8c5f9fdd6c7003a88d59bedeaf8752716a55f532924fbb/h5py-3.16.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2b2c02b0a160faed5fb33f1ba8a264a37ee240b22e049ecc827345d0d9043074", size = 5419216, upload-time = "2026-03-06T13:48:13.322Z" }, + { url = "https://files.pythonhosted.org/packages/03/c1/0976b235cf29ead553e22f2fb6385a8252b533715e00d0ae52ed7b900582/h5py-3.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:96b422019a1c8975c2d5dadcf61d4ba6f01c31f92bbde6e4649607885fe502d6", size = 3182868, upload-time = "2026-03-06T13:48:15.759Z" }, { url = "https://files.pythonhosted.org/packages/0f/9e/6142ebfda0cb6e9349c091eae73c2e01a770b7659255248d637bec54a88b/h5py-3.16.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:370a845f432c2c9619db8eed334d1e610c6015796122b0e57aa46312c22617d9", size = 3671808, upload-time = "2026-03-06T13:48:19.737Z" }, { url = "https://files.pythonhosted.org/packages/b0/65/5e088a45d0f43cd814bc5bec521c051d42005a472e804b1a36c48dada09b/h5py-3.16.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:42108e93326c50c2810025aade9eac9d6827524cdccc7d4b75a546e5ab308edb", size = 3045837, upload-time = "2026-03-06T13:48:21.854Z" }, { url = "https://files.pythonhosted.org/packages/da/1e/6172269e18cc5a484e2913ced33339aad588e02ba407fafd00d369e22ef3/h5py-3.16.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:099f2525c9dcf28de366970a5fb34879aab20491589fa89ce2863a84218bb524", size = 5193860, upload-time = "2026-03-06T13:48:24.071Z" }, { url = "https://files.pythonhosted.org/packages/bd/98/ef2b6fe2903e377cbe870c3b2800d62552f1e3dbe81ce49e1923c53d1c5c/h5py-3.16.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:9300ad32dea9dfc5171f94d5f6948e159ed93e4701280b0f508773b3f582f402", size = 5400417, upload-time = "2026-03-06T13:48:25.728Z" }, { url = "https://files.pythonhosted.org/packages/bc/81/5b62d760039eed64348c98129d17061fdfc7839fc9c04eaaad6dee1004e4/h5py-3.16.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:171038f23bccddfc23f344cadabdfc9917ff554db6a0d417180d2747fe4c75a7", size = 5185214, upload-time = "2026-03-06T13:48:27.436Z" }, { url = "https://files.pythonhosted.org/packages/28/c4/532123bcd9080e250696779c927f2cb906c8bf3447df98f5ceb8dcded539/h5py-3.16.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7e420b539fb6023a259a1b14d4c9f6df8cf50d7268f48e161169987a57b737ff", size = 5414598, upload-time = "2026-03-06T13:48:29.49Z" }, + { url = "https://files.pythonhosted.org/packages/c3/d9/a27997f84341fc0dfcdd1fe4179b6ba6c32a7aa880fdb8c514d4dad6fba3/h5py-3.16.0-cp313-cp313-win_amd64.whl", hash = "sha256:18f2bbcd545e6991412253b98727374c356d67caa920e68dc79eab36bf5fedad", size = 3175509, upload-time = "2026-03-06T13:48:31.131Z" }, { url = "https://files.pythonhosted.org/packages/48/3c/7fcd9b4c9eed82e91fb15568992561019ae7a829d1f696b2c844355d95dd/h5py-3.16.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:9c9d307c0ef862d1cd5714f72ecfafe0a5d7529c44845afa8de9f46e5ba8bd65", size = 3678608, upload-time = "2026-03-06T13:48:35.183Z" }, { url = "https://files.pythonhosted.org/packages/6a/b7/9366ed44ced9b7ef357ab48c94205280276db9d7f064aa3012a97227e966/h5py-3.16.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:8c1eff849cdd53cbc73c214c30ebdb6f1bb8b64790b4b4fc36acdb5e43570210", size = 3054773, upload-time = "2026-03-06T13:48:37.139Z" }, { url = "https://files.pythonhosted.org/packages/58/a5/4964bc0e91e86340c2bbda83420225b2f770dcf1eb8a39464871ad769436/h5py-3.16.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:e2c04d129f180019e216ee5f9c40b78a418634091c8782e1f723a6ca3658b965", size = 5198886, upload-time = "2026-03-06T13:48:38.879Z" }, { url = "https://files.pythonhosted.org/packages/f1/16/d905e7f53e661ce2c24686c38048d8e2b750ffc4350009d41c4e6c6c9826/h5py-3.16.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:e4360f15875a532bc7b98196c7592ed4fc92672a57c0a621355961cafb17a6dd", size = 5404883, upload-time = "2026-03-06T13:48:41.324Z" }, { url = "https://files.pythonhosted.org/packages/4b/f2/58f34cb74af46d39f4cd18ea20909a8514960c5a3e5b92fd06a28161e0a8/h5py-3.16.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:3fae9197390c325e62e0a1aa977f2f62d994aa87aab182abbea85479b791197c", size = 5192039, upload-time = "2026-03-06T13:48:43.117Z" }, { url = "https://files.pythonhosted.org/packages/ce/ca/934a39c24ce2e2db017268c08da0537c20fa0be7e1549be3e977313fc8f5/h5py-3.16.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:43259303989ac8adacc9986695b31e35dba6fd1e297ff9c6a04b7da5542139cc", size = 5421526, upload-time = "2026-03-06T13:48:44.838Z" }, + { url = "https://files.pythonhosted.org/packages/3e/14/615a450205e1b56d16c6783f5ccd116cde05550faad70ae077c955654a75/h5py-3.16.0-cp314-cp314-win_amd64.whl", hash = "sha256:fa48993a0b799737ba7fd21e2350fa0a60701e58180fae9f2de834bc39a147ab", size = 3183263, upload-time = "2026-03-06T13:48:47.117Z" }, { url = "https://files.pythonhosted.org/packages/5d/32/0c8bb8aedb62c772cf7c1d427c7d1951477e8c2835f872bc0a13d1f85f86/h5py-3.16.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:15922e485844f77c0b9d275396d435db3baa58292a9c2176a386e072e0cf2491", size = 3760693, upload-time = "2026-03-06T13:48:50.453Z" }, { url = "https://files.pythonhosted.org/packages/1d/1f/fcc5977d32d6387c5c9a694afee716a5e20658ac08b3ff24fdec79fb05f2/h5py-3.16.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:df02dd29bd247f98674634dfe41f89fd7c16ba3d7de8695ec958f58404a4e618", size = 3181305, upload-time = "2026-03-06T13:48:52.221Z" }, { url = "https://files.pythonhosted.org/packages/f5/a1/af87f64b9f986889884243643621ebbd4ac72472ba8ec8cec891ac8e2ca1/h5py-3.16.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:0f456f556e4e2cebeebd9d66adf8dc321770a42593494a0b6f0af54a7567b242", size = 5074061, upload-time = "2026-03-06T13:48:54.089Z" }, { url = "https://files.pythonhosted.org/packages/cc/d0/146f5eaff3dc246a9c7f6e5e4f42bd45cc613bce16693bcd4d1f7c958bf5/h5py-3.16.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:3e6cb3387c756de6a9492d601553dffea3fe11b5f22b443aac708c69f3f55e16", size = 5279216, upload-time = "2026-03-06T13:48:56.75Z" }, { url = "https://files.pythonhosted.org/packages/a1/9d/12a13424f1e604fc7df9497b73c0356fb78c2fb206abd7465ce47226e8fd/h5py-3.16.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8389e13a1fd745ad2856873e8187fd10268b2d9677877bb667b41aebd771d8b7", size = 5070068, upload-time = "2026-03-06T13:48:59.169Z" }, { url = "https://files.pythonhosted.org/packages/41/8c/bbe98f813722b4873818a8db3e15aa3e625b59278566905ac439725e8070/h5py-3.16.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:346df559a0f7dcb31cf8e44805319e2ab24b8957c45e7708ce503b2ec79ba725", size = 5300253, upload-time = "2026-03-06T13:49:02.033Z" }, + { url = "https://files.pythonhosted.org/packages/32/9e/87e6705b4d6890e7cecdf876e2a7d3e40654a2ae37482d79a6f1b87f7b92/h5py-3.16.0-cp314-cp314t-win_amd64.whl", hash = "sha256:4c6ab014ab704b4feaa719ae783b86522ed0bf1f82184704ed3c9e4e3228796e", size = 3381671, upload-time = "2026-03-06T13:49:04.351Z" }, ] [[package]] @@ -1482,20 +1512,22 @@ wheels = [ [[package]] name = "pyscf" -version = "2.14.0" +version = "2.15.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "h5py" }, { name = "numpy" }, + { name = "psutil", marker = "sys_platform == 'win32'" }, { name = "scipy" }, { name = "setuptools" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/3b/76/4b4f33877f4ee460733bd8b9af64cb9c13be9d5111fb8f6357d227c2c908/pyscf-2.14.0.tar.gz", hash = "sha256:9b6ea3c2470baac9d0492d93b335542c04ab96277e79f1abcbe1cea0eb063c5f", size = 11565773, upload-time = "2026-07-18T22:50:28.053Z" } +sdist = { url = "https://files.pythonhosted.org/packages/6e/30/39707040412f20c61a91a7edcd921a5973c9b604c4d6095c2e0a5f7b35d3/pyscf-2.15.0.tar.gz", hash = "sha256:46601b24bb5fa8583f4734c65f096f1cb2006cd1a2c581b7761e1e740f36d74f", size = 10941454, upload-time = "2026-11-05T05:42:06.643Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2f/6e/16109bef2ed8b276c954e5a951e14eaa501d8f79ee928b64bc6fd0d2b440/pyscf-2.14.0-py3-none-macosx_10_9_x86_64.whl", hash = "sha256:2f4964d64c5cbb7eaa363ed8a5d1b7cc8541f2008b9c3c81c22494d789fbcd33", size = 37156346, upload-time = "2026-07-18T22:27:48.81Z" }, - { url = "https://files.pythonhosted.org/packages/48/ed/bb4aeb728505b4ce7edd55e9b2e35bcbd99ad0b0cae5c45b59e48b9ed62d/pyscf-2.14.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:a4bdc6f4155e258d78c90b4de0c88ab9260a45801264d6c471c3beec83a4da4c", size = 36492544, upload-time = "2026-07-18T22:24:50.025Z" }, - { url = "https://files.pythonhosted.org/packages/89/f2/24c58dad0c33ab417c5a090695a6788292d5d4edceae59bad055e5377e92/pyscf-2.14.0-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2c01234a5480e545e5e156d1fabe9b97264bf87ee274c62d14478ab9fe644100", size = 46031836, upload-time = "2026-07-19T00:57:07.889Z" }, - { url = "https://files.pythonhosted.org/packages/88/30/8e48717aff32f11fd78f981192d0b567971eb285ea048414b7dac2a211cf/pyscf-2.14.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:37b0bccc55450311a55318cd643e851353331ddeab4fc0c0065e83c905e41502", size = 52848663, upload-time = "2026-07-18T22:49:58.707Z" }, + { url = "https://files.pythonhosted.org/packages/70/ff/a8d1086c7029aa05e370b015f364e421a148af7fb6969b00ef5565a04eeb/pyscf-2.15.0-py3-none-macosx_10_9_x86_64.whl", hash = "sha256:3640c34e938a09c4211fda448ee857374cf8a10eb345547f003ee25650d26392", size = 25301270, upload-time = "2026-11-05T05:41:29.587Z" }, + { url = "https://files.pythonhosted.org/packages/9c/e8/579b0353de0ba84621a2530fcccc4ca7ef9ca78e05697c1c4e3ce319a88a/pyscf-2.15.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:727f854866e7930f1c28722030a25efff61f594fcca78735e60c1a62829a4e62", size = 23364845, upload-time = "2026-11-05T05:37:05.523Z" }, + { url = "https://files.pythonhosted.org/packages/2f/1d/7e4e878248fae458f4934dc6f31ae2cbcf5f67230d6b4505b3655c8559bb/pyscf-2.15.0-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ca9427ae9f41c03e44c74763cc61d240e46be9a7cc75166c22817eebf1e0b87b", size = 33597044, upload-time = "2026-11-05T06:43:39.266Z" }, + { url = "https://files.pythonhosted.org/packages/bd/c4/ae16e0cf2d391e87bfa0e81a764fb5847253eabf1e8bdaf07959244c73e2/pyscf-2.15.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a372e4ac136b969ef4a22fb328905cfbc84513bcd4d85321026af0493345b94b", size = 40598332, upload-time = "2026-11-05T05:41:36.196Z" }, + { url = "https://files.pythonhosted.org/packages/7e/09/aaf029f524f4cf186a98e3d084ea81e2b9ee156d6e6fddc4fad17e5e99a4/pyscf-2.15.0-py3-none-win_amd64.whl", hash = "sha256:dadffed2a2e547d5d95d8ad7e96be7ef480ed73999b497c2f343c13cb278197d", size = 75095110, upload-time = "2026-11-05T05:45:23.381Z" }, ] [[package]] @@ -1807,11 +1839,12 @@ version = "0.0.0" source = { virtual = "." } default-groups = ["dev", "linting", "test"] dependencies = [ - { name = "pyscf", marker = "sys_platform != 'win32'" }, + { name = "pyscf", marker = "platform_machine == 'AMD64' or platform_machine == 'x86_64' or sys_platform != 'win32'" }, ] [package.dev-dependencies] dev = [ + { name = "argon2-cffi-bindings", version = "25.1.0", source = { registry = "https://pypi.org/simple" }, marker = "platform_machine == 'x86_64' and sys_platform != 'linux' and sys_platform != 'win32'" }, { name = "ipykernel" }, { name = "jupyterlab" }, { name = "matplotlib" }, @@ -1826,10 +1859,11 @@ test = [ ] [package.metadata] -requires-dist = [{ name = "pyscf", marker = "sys_platform != 'win32'", specifier = "==2.14.0" }] +requires-dist = [{ name = "pyscf", marker = "platform_machine == 'AMD64' or platform_machine == 'x86_64' or sys_platform != 'win32'", specifier = "==2.15.0" }] [package.metadata.requires-dev] dev = [ + { name = "argon2-cffi-bindings", marker = "platform_machine == 'x86_64' and sys_platform == 'darwin'", specifier = "==25.1.0" }, { name = "ipykernel" }, { name = "jupyterlab" }, { name = "matplotlib" }, From eb1e866fc2fa4818041c4f5edf7e26ab5f3a04ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20V=C3=A9ril?= Date: Thu, 12 Nov 2026 15:07:20 +0100 Subject: [PATCH 09/12] Restrict generated API documentation (#213) --- Cargo.toml | 1 + THIRD-PARTY-NOTICES.sha256 | 2 +- THIRD-PARTY-NOTICES.txt | 2 +- .../rustiq-core/src/hf/density_guess/one_electron.rs | 4 ++-- crates/rustiq-core/src/hf/scf_setup.rs | 2 +- crates/rustiq-core/src/math_utils.rs | 11 ++++++----- 6 files changed, 12 insertions(+), 10 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 44d0bf86..9f62cfb7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -80,6 +80,7 @@ categories = ["science::computational-chemistry::electronic-structure", "command [[bin]] name = "rustiq" path = "src/main.rs" +doc = false [features] default = ["online", "manpages", "completions"] diff --git a/THIRD-PARTY-NOTICES.sha256 b/THIRD-PARTY-NOTICES.sha256 index 4dd906b4..260cd661 100644 --- a/THIRD-PARTY-NOTICES.sha256 +++ b/THIRD-PARTY-NOTICES.sha256 @@ -1 +1 @@ -fd10896a1c4a5a5be0dfdcabb49197b9fb5dc80a93d9e0e8a917ff746ca3169e THIRD-PARTY-NOTICES.txt +2fbcaed6277c0ee1532734dbf6474fea1965985e94c34c8fe07ab9ea0dbaf992 THIRD-PARTY-NOTICES.txt diff --git a/THIRD-PARTY-NOTICES.txt b/THIRD-PARTY-NOTICES.txt index 37b26622..d065bf9f 100644 --- a/THIRD-PARTY-NOTICES.txt +++ b/THIRD-PARTY-NOTICES.txt @@ -1,5 +1,5 @@ Cargo.lock SHA-256: 7f274ca9e7f738d58ec0e46d03be9802569df0512ec91892f4ccd1edbd90ad02 -Notice inputs SHA-256: f3b7a59fa3633c0574f9c0f0b1c6a0b767056d57307cba53913adf72ef06f236 +Notice inputs SHA-256: 23de7d7b750a771f73732580c0542ac4047e5afd0d13789e3ec333efed68208c Corresponding source: https://github.com/mveril/RustiQ/releases/download/v0.1.0-alpha.1.dev/rustiq-corresponding-source.tar.gz RustiQ third-party licenses diff --git a/crates/rustiq-core/src/hf/density_guess/one_electron.rs b/crates/rustiq-core/src/hf/density_guess/one_electron.rs index b0bb780e..42fed3da 100644 --- a/crates/rustiq-core/src/hf/density_guess/one_electron.rs +++ b/crates/rustiq-core/src/hf/density_guess/one_electron.rs @@ -25,7 +25,7 @@ impl DensityGuess for OneElectron { h_core: &DMatrix, _basis: &Basis, ) -> Result, Self::Error> { - crate::debug_assert_is_symmetric!(h_core, 1e-8); + crate::math_utils::debug_assert_is_symmetric!(h_core, 1e-8); match self.perturbation { Some(perturbation) => { perturb_fock_like_matrices(h_core, perturbation).map_err(Into::into) @@ -101,7 +101,7 @@ mod tests { let density = scf.density_matrix; // Check that the density is symmetric - crate::debug_assert_is_symmetric!(&density, 1e-8); + crate::math_utils::debug_assert_is_symmetric!(&density, 1e-8); // AO densities are normalized in the overlap metric, not the Euclidean trace. let trace = (&density * basis.overlap_ints()).trace(); diff --git a/crates/rustiq-core/src/hf/scf_setup.rs b/crates/rustiq-core/src/hf/scf_setup.rs index af1df475..eca27a66 100644 --- a/crates/rustiq-core/src/hf/scf_setup.rs +++ b/crates/rustiq-core/src/hf/scf_setup.rs @@ -135,7 +135,7 @@ fn prepare_scf_setup_with_thresholds( let step_start = Instant::now(); let overlap = builder.overlap(); timings.overlap = step_start.elapsed(); - crate::debug_assert_is_symmetric!(&overlap, 1e-8); + crate::math_utils::debug_assert_is_symmetric!(&overlap, 1e-8); progress(ScfSetupStep::OverlapOrthogonalizer); let step_start = Instant::now(); diff --git a/crates/rustiq-core/src/math_utils.rs b/crates/rustiq-core/src/math_utils.rs index 118ed120..2c1a8266 100644 --- a/crates/rustiq-core/src/math_utils.rs +++ b/crates/rustiq-core/src/math_utils.rs @@ -67,7 +67,6 @@ pub fn hermite(n: u64, PA: u32, QA: u32, p: f64, q: f64, f0: f64) -> f64 { f0 * hermite_classic(n, x) } -#[macro_export] #[cfg(debug_assertions)] macro_rules! debug_assert_is_symmetric { ($matrix:expr, $tol:expr) => {{ @@ -98,12 +97,13 @@ macro_rules! debug_assert_is_symmetric { }}; } -#[macro_export] #[cfg(not(debug_assertions))] macro_rules! debug_assert_is_symmetric { ($matrix:expr, $tol:expr) => {{}}; } +pub(crate) use debug_assert_is_symmetric; + #[cfg(test)] pub(crate) fn is_positive_definite(matrix: &DMatrix) -> bool { matrix @@ -162,11 +162,12 @@ mod tests { #[cfg(debug_assertions)] { let mat = DMatrix::from_row_slice(2, 2, &[1.0, 2.0, 2.0, 1.0]); - crate::debug_assert_is_symmetric!(&mat, 1e-12); // Should not fail + crate::math_utils::debug_assert_is_symmetric!(&mat, 1e-12); // Should not fail let mat_non_sym = DMatrix::from_row_slice(2, 2, &[1.0, 3.0, 2.0, 1.0]); // Corrected to make it non-symmetric - let result = - std::panic::catch_unwind(|| crate::debug_assert_is_symmetric!(&mat_non_sym, 1e-12)); + let result = std::panic::catch_unwind(|| { + crate::math_utils::debug_assert_is_symmetric!(&mat_non_sym, 1e-12); + }); assert!(result.is_err(), "The matrix should not be symmetric"); } } From b0bdd3628d5f87c524ee99b1f434ff6ae84c87b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20V=C3=A9ril?= Date: Thu, 12 Nov 2026 15:06:25 +0100 Subject: [PATCH 10/12] Prepare release v0.1.0-beta.1 --- .github/ISSUE_TEMPLATE/01_bug_report.yml | 2 +- .github/workflows/release-smoke.yml | 6 ++ .github/workflows/release.yml | 2 +- CHANGELOG.md | 6 +- Cargo.lock | 5 +- Cargo.toml | 4 +- README.md | 8 +- THIRD-PARTY-NOTICES.sha256 | 2 +- THIRD-PARTY-NOTICES.txt | 10 +- crates/rustiq-core/Cargo.toml | 5 +- docs/src/development/docker-setup.md | 2 +- docs/src/development/manual-setup.md | 2 +- docs/src/development/nix-packages.md | 7 +- docs/src/development/nix-setup.md | 4 +- docs/src/development/releasing.md | 22 +++-- docs/src/getting-started/first-calculation.md | 7 +- docs/src/getting-started/installation-nix.md | 51 +++++++---- docs/src/getting-started/installation.md | 91 ++++++++++++++----- docs/src/getting-started/rustiq.nix | 2 +- docs/src/reference/cli.md | 2 +- 20 files changed, 163 insertions(+), 77 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/01_bug_report.yml b/.github/ISSUE_TEMPLATE/01_bug_report.yml index 58243e06..a7e62222 100644 --- a/.github/ISSUE_TEMPLATE/01_bug_report.yml +++ b/.github/ISSUE_TEMPLATE/01_bug_report.yml @@ -84,7 +84,7 @@ body: id: rustiq_version attributes: label: RustiQ version or commit - placeholder: v0.1.0-alpha.1 or commit SHA + placeholder: v0.1.0-beta.1 or commit SHA validations: required: true diff --git a/.github/workflows/release-smoke.yml b/.github/workflows/release-smoke.yml index dccfa305..89d93b60 100644 --- a/.github/workflows/release-smoke.yml +++ b/.github/workflows/release-smoke.yml @@ -1,6 +1,8 @@ name: Release artifact smoke test on: + pull_request: + branches: [main] workflow_dispatch: inputs: ref: @@ -12,6 +14,10 @@ on: permissions: contents: read +concurrency: + group: release-smoke-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + jobs: manual: uses: ./.github/workflows/build-release-manual.yml diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 1e6abc2f..bf96cdec 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -95,7 +95,7 @@ jobs: # Build and packages all the platform-specific things build-local-artifacts: - name: build-local-artifacts (${{ join(matrix.targets, ', ') }}) + name: Build platform artifacts # Let the initial task tell us to not run (currently very blunt) needs: - plan diff --git a/CHANGELOG.md b/CHANGELOG.md index 0fd7d9f2..fc3391ae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] -The following capabilities are intended for the first public beta, `v0.1.0-beta.1`. +## [0.1.0-beta.1] - 2026-11-12 + ### Added @@ -58,4 +59,5 @@ The following capabilities are intended for the first public beta, `v0.1.0-beta. - The MP2 workspace budget does not bound total process memory; AO integral storage can still grow rapidly with basis size. -[Unreleased]: https://github.com/mveril/RustiQ/commits/main/ +[Unreleased]: https://github.com/mveril/RustiQ/compare/v0.1.0-beta.1...HEAD +[0.1.0-beta.1]: https://github.com/mveril/RustiQ/releases/tag/v0.1.0-beta.1 diff --git a/Cargo.lock b/Cargo.lock index c15a6222..d2a666e2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4675,7 +4675,7 @@ dependencies = [ [[package]] name = "rustiq-chem" -version = "0.1.0-alpha.1.dev" +version = "0.1.0-beta.1" dependencies = [ "anstream", "anstyle", @@ -4719,7 +4719,7 @@ dependencies = [ [[package]] name = "rustiq-chem-core" -version = "0.1.0-alpha.1.dev" +version = "0.1.0-beta.1" dependencies = [ "approx", "bounded-vec-deque", @@ -4727,6 +4727,7 @@ dependencies = [ "const_format", "delegate", "factorial", + "itertools 0.14.0", "mendeleev", "miette", "nalgebra", diff --git a/Cargo.toml b/Cargo.toml index 9f62cfb7..4a3e9125 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -67,7 +67,7 @@ tokio = { version = "1", default-features = false } name = "rustiq-chem" description = "A modern command-line application for quantum chemistry, written in Rust" authors.workspace = true -version = "0.1.0-alpha.1.dev" +version = "0.1.0-beta.1" edition.workspace = true license.workspace = true repository.workspace = true @@ -91,7 +91,7 @@ completions = ["dep:clap_complete", "dep:clap_complete_nushell", "dep:sysinfo"] [dependencies] bytesize.workspace = true -rustiq-core = { package = "rustiq-chem-core", path = "crates/rustiq-core", version = "0.1.0-alpha.1.dev", default-features = false } +rustiq-core = { package = "rustiq-chem-core", path = "crates/rustiq-core", version = "0.1.0-beta.1", default-features = false } directories = "6" miette = { workspace = true, features = ["fancy"] } clap = { version = "4.6", features = ["cargo", "derive", "env"] } diff --git a/README.md b/README.md index 3a6bc891..8e5fd2a0 100644 --- a/README.md +++ b/README.md @@ -34,12 +34,18 @@ and [the first H₂ calculation](https://mveril.github.io/RustiQ/getting-started Release archives contain the executable and licenses; obtain basis data and create calculation inputs as shown in those guides. +Once the beta is published, install it from crates.io with +`cargo install --locked rustiq-chem --version '=0.1.0-beta.1'`, or install the +tagged flake’s default package with +`nix profile install 'github:mveril/RustiQ/v0.1.0-beta.1'`. +See the installation guide for prerequisites and supported Nix platforms. + To build from source, install Git, Rust through [rustup](https://rustup.rs/), a C/C++ build toolchain, and CMake; see [manual setup](docs/src/development/manual-setup.md). Alternatively, enter `nix develop .#rust` after cloning: ```sh -git clone https://github.com/mveril/RustiQ.git +git clone --branch develop --single-branch https://github.com/mveril/RustiQ.git cd RustiQ cargo build --locked cargo run --locked -- basis import tests/data/sto-3g.json diff --git a/THIRD-PARTY-NOTICES.sha256 b/THIRD-PARTY-NOTICES.sha256 index 260cd661..7df2b17e 100644 --- a/THIRD-PARTY-NOTICES.sha256 +++ b/THIRD-PARTY-NOTICES.sha256 @@ -1 +1 @@ -2fbcaed6277c0ee1532734dbf6474fea1965985e94c34c8fe07ab9ea0dbaf992 THIRD-PARTY-NOTICES.txt +d1447dd924e4c3c979f72ea91ef98554fc96dc7581091097abb5f52193956343 THIRD-PARTY-NOTICES.txt diff --git a/THIRD-PARTY-NOTICES.txt b/THIRD-PARTY-NOTICES.txt index d065bf9f..e3d1f679 100644 --- a/THIRD-PARTY-NOTICES.txt +++ b/THIRD-PARTY-NOTICES.txt @@ -1,6 +1,6 @@ -Cargo.lock SHA-256: 7f274ca9e7f738d58ec0e46d03be9802569df0512ec91892f4ccd1edbd90ad02 -Notice inputs SHA-256: 23de7d7b750a771f73732580c0542ac4047e5afd0d13789e3ec333efed68208c -Corresponding source: https://github.com/mveril/RustiQ/releases/download/v0.1.0-alpha.1.dev/rustiq-corresponding-source.tar.gz +Cargo.lock SHA-256: 516ff985afb11ee01ab084d5bf4e1e88ab0b52a9517ee03be38757a1d9a34c13 +Notice inputs SHA-256: ece10609b6fe7c51fc664ec00c7614d06eb230fca4ceca36479e1671978f0d8b +Corresponding source: https://github.com/mveril/RustiQ/releases/download/v0.1.0-beta.1/rustiq-corresponding-source.tar.gz RustiQ third-party licenses ========================== @@ -8220,8 +8220,8 @@ limitations under the License. Apache License 2.0 (Apache-2.0) Used by: - rustiq-chem 0.1.0-alpha.1.dev — https://github.com/mveril/RustiQ - rustiq-chem-core 0.1.0-alpha.1.dev — https://github.com/mveril/RustiQ + rustiq-chem 0.1.0-beta.1 — https://github.com/mveril/RustiQ + rustiq-chem-core 0.1.0-beta.1 — https://github.com/mveril/RustiQ Apache License Version 2.0, January 2004 diff --git a/crates/rustiq-core/Cargo.toml b/crates/rustiq-core/Cargo.toml index 48482916..8fc8fbef 100644 --- a/crates/rustiq-core/Cargo.toml +++ b/crates/rustiq-core/Cargo.toml @@ -2,7 +2,7 @@ name = "rustiq-chem-core" description = "A reusable Rust library for modern quantum chemistry software" authors.workspace = true -version = "0.1.0-alpha.1.dev" +version = "0.1.0-beta.1" edition.workspace = true license.workspace = true repository.workspace = true @@ -28,6 +28,7 @@ online = ["dep:reqwest", "dep:tokio"] bench = [] [dependencies] +itertools = { version = "0.14", default-features = false } miette.workspace = true reqwest = { version = "0.13", default-features = false, optional = true, features = [ "json", @@ -48,7 +49,7 @@ bounded-vec-deque = "0.1.1" thiserror.workspace = true rand.workspace = true rand_chacha = "0.10" -petname = { version = "3.2", default-features = false } +petname = { version = "3.1", default-features = false } rand_distr = "0.6" special = { version = "0.14", default-features = false, features = ["std"] } factorial = "0.4.0" diff --git a/docs/src/development/docker-setup.md b/docs/src/development/docker-setup.md index 2b045483..a7d6372d 100644 --- a/docs/src/development/docker-setup.md +++ b/docs/src/development/docker-setup.md @@ -63,7 +63,7 @@ cross-architecture emulation into the comparison. Open a terminal on your computer and download the project: ```sh -git clone https://github.com/mveril/RustiQ.git +git clone --branch develop --single-branch https://github.com/mveril/RustiQ.git cd RustiQ ``` diff --git a/docs/src/development/manual-setup.md b/docs/src/development/manual-setup.md index d0ec5b66..7d8c90be 100644 --- a/docs/src/development/manual-setup.md +++ b/docs/src/development/manual-setup.md @@ -27,7 +27,7 @@ Install Git and Rust through [`rustup`](https://rustup.rs/), then use Cargo directly on Linux, macOS, or Windows: ```sh -git clone https://github.com/mveril/RustiQ.git +git clone --branch develop --single-branch https://github.com/mveril/RustiQ.git cd RustiQ rustup show cargo build --locked diff --git a/docs/src/development/nix-packages.md b/docs/src/development/nix-packages.md index 992060bb..fef796cd 100644 --- a/docs/src/development/nix-packages.md +++ b/docs/src/development/nix-packages.md @@ -2,7 +2,7 @@ The flake also exposes `legacyPackages..cpuTarget`, a function accepting an LLVM CPU name and returning a CPU-specific RustiQ package. See the -[installation guide](../getting-started/installation.md#install-with-nix) for +[Nix installation guide](../getting-started/installation-nix.md#optional-cpu-specific-build) for commands and portability considerations. The default package remains portable. The packages below are declared in [`flake.nix`](https://github.com/mveril/RustiQ/blob/main/flake.nix). All four @@ -96,7 +96,10 @@ The flake also exposes `nix build .#book` to build this manual, `nix build .#cargo-artifacts` for cached Cargo dependency artifacts, and `nix build .#pyscf-environment` for the base PySCF environment. These are build outputs rather than development shells. The default package is the RustiQ CLI -binary, available through `nix build`. Its online manual follows the Cargo +binary, available through `nix build` or explicitly through `nix build .#default`. +Install a tagged version with `nix profile install github:mveril/RustiQ/vVERSION`, +replacing `vVERSION` with a published tag. Both development and release revisions +use this default package. Its online manual follows the Cargo version: a `dev` prerelease identifier selects development documentation; other versions select the matching versioned manual. No release flag is needed. See [releasing](releasing.md#preparing-a-release). diff --git a/docs/src/development/nix-setup.md b/docs/src/development/nix-setup.md index 9061249e..00c8e01c 100644 --- a/docs/src/development/nix-setup.md +++ b/docs/src/development/nix-setup.md @@ -94,7 +94,7 @@ Install Nix with flakes enabled, clone the repository, and enter the development shell manually: ```sh -git clone https://github.com/mveril/RustiQ.git +git clone --branch develop --single-branch https://github.com/mveril/RustiQ.git cd RustiQ nix develop ``` @@ -144,7 +144,7 @@ nix build Install both Nix and `direnv`, enable the direnv hook for your shell, then run: ```sh -git clone https://github.com/mveril/RustiQ.git +git clone --branch develop --single-branch https://github.com/mveril/RustiQ.git cd RustiQ direnv allow ``` diff --git a/docs/src/development/releasing.md b/docs/src/development/releasing.md index 5013d9d5..20782557 100644 --- a/docs/src/development/releasing.md +++ b/docs/src/development/releasing.md @@ -33,13 +33,17 @@ out-of-date generated workflow. Keep the pin synchronized with the intended upload-artifact release when updating the workflow or cargo-dist. Preserve the custom `build-manual` job, the local-build dependency on it, and the download into `manual/html/` when regenerating the release workflow; generated defaults do -not contain this offline-manual integration. +not contain this offline-manual integration. Preserve the static local-build job +name, `Build platform artifacts`, so pull requests that skip artifact builds +display a readable name before the target matrix is expanded. ### Release rehearsal -Run **Actions → Release artifact smoke test → Run workflow** and select the -commit, branch, or tag to rehearse. The manually triggered workflow builds the -configured target archive with the pinned dist version on each target's native +The **Release artifact smoke test** workflow runs automatically on pull requests +targeting `main`, testing the prospective merge commit. To rehearse another +revision, run **Actions → Release artifact smoke test → Run workflow** and select +the commit, branch, or tag. New runs cancel older rehearsals for the same pull +request or selected workflow ref. The workflow builds the configured target archive with the pinned dist version on each target's native GitHub-hosted runner. It extracts that archive, verifies its dist-generated SHA-256 checksum, starts the packaged `rustiq` executable, checks `--help`, and runs the checked-in H2/STO-3G regression with a copied local basis fixture @@ -68,12 +72,12 @@ system installations. The rehearsal requires the combined `sha256.sum` generated dist build and verifies each tested archive against it. A checksum made during a rehearsal does not independently validate a published release. -The rehearsal has read-only repository permissions and only a -`workflow_dispatch` trigger. It does not run dist's `host` command, upload +The rehearsal has read-only repository permissions and supports `pull_request` +events targeting `main` and manual `workflow_dispatch` runs. It does not run dist's `host` command, upload release assets, create a GitHub Release, create tags, or publish crates. It complements `.github/workflows/release.yml`, whose dispatched release path builds -and publishes the release assets; ordinary pull requests continue to use the -generated plan-only check. +and publishes the release assets. Pull requests targeting `develop` continue +to use the generated plan-only check without the six-target rehearsal. Each executable archive includes `rustiq` (named `rustiq.exe` on Windows), the README, `LICENSE-MIT`, `LICENSE-APACHE`, `COPYING`, `DISTRIBUTION.md`, and @@ -268,7 +272,7 @@ if the Nixpkgs package differs: cargo install cargo-dist --version 0.34.0 --locked ``` -Development takes place on the default `develop` branch. Use a version ending +Development takes place on the `develop` branch. Use a version ending in `.dev`, keeping both crate versions and the internal dependency synchronized. Prepare a release on a `chore/*` task branch from `develop`: remove the development identifier, finalize the dated changelog, and update `Cargo.lock`. diff --git a/docs/src/getting-started/first-calculation.md b/docs/src/getting-started/first-calculation.md index 3ba87fca..3b969aa9 100644 --- a/docs/src/getting-started/first-calculation.md +++ b/docs/src/getting-started/first-calculation.md @@ -1,9 +1,10 @@ # First H₂ calculation -## Using only the release binary +## Using an installed release -Copy the following geometry into a file named `h2.xyz` in your working -directory. It is the same geometry used by the checked-in sample: +The following commands work after installing a release from a GitHub archive, +Cargo, or Nix. Copy the following geometry into a file named `h2.xyz` in your +working directory. It is the same geometry used by the checked-in sample: ```text {{#include ../../../samples/h2/molecule.xyz}} diff --git a/docs/src/getting-started/installation-nix.md b/docs/src/getting-started/installation-nix.md index a9709ea0..5860c286 100644 --- a/docs/src/getting-started/installation-nix.md +++ b/docs/src/getting-started/installation-nix.md @@ -1,5 +1,10 @@ # Installing with Nix +Nix is one of the three release installation channels, alongside a +[GitHub Releases archive](installation.md#install-a-prebuilt-release-binary) +and the [Cargo package](installation.md#install-with-cargo). This page covers +the Nix-specific choices. + Install [Nix](https://nixos.org/download/) and enable the `nix-command` and `flakes` experimental features. The flake supports x86-64 Linux, ARM64 Linux, and Apple Silicon macOS. On Windows, use Nix inside WSL2 to run the Linux @@ -12,6 +17,13 @@ nix profile install github:mveril/RustiQ rustiq --version ``` +The unqualified `github:mveril/RustiQ` reference follows GitHub's default +branch, `main`. That branch contains released versions only. To keep a fixed +version, use a release tag (for example, +`github:mveril/RustiQ/v0.1.0-beta.1`) or a commit revision instead. +An unqualified reference is captured only when a consuming configuration's +`flake.lock` is updated; a tag makes the selected release explicit. + ## Home Manager without a configuration flake If your Home Manager configuration uses `home.nix` directly, create a @@ -24,7 +36,7 @@ let in { home.packages = [ - (rustiq.legacyPackages.${pkgs.stdenv.hostPlatform.system}.cpuTarget "native") + rustiq.packages.${pkgs.stdenv.hostPlatform.system}.default ]; } ``` @@ -38,11 +50,11 @@ imports = [ ./rustiq.nix ]; Apply the configuration using your usual command. For Home Manager integrated as a NixOS module, run `sudo nixos-rebuild switch --impure`; for standalone Home Manager, run `home-manager switch --impure`. The unlocked GitHub reference -requires impure evaluation. Check `rustiq --version` afterward. The reference tracks the default branch -and is not pinned by a configuration `flake.lock`; use a revision in the URL -for a fixed version. See the [CPU-specific build](#optional-cpu-specific-build) -caveats before using `native`. To use the standard package, replace the entry -in `home.packages` with `rustiq.packages.${pkgs.stdenv.hostPlatform.system}.default`. +requires impure evaluation. Check `rustiq --version` afterward. The reference tracks +the release-only default branch, `main`, and is not pinned by a configuration +`flake.lock`; use a revision in the URL +for a fixed version. The standard package is portable across machines of the +same supported platform. ## Home Manager and NixOS @@ -97,8 +109,8 @@ specialArgs = { inherit inputs; }; ``` Merge this with any existing `specialArgs`. Create `rustiq.nix` beside your -`configuration.nix`. This complete NixOS module installs a RustiQ build -optimized for the build machine's CPU: +`configuration.nix`. This complete NixOS module installs the standard portable +RustiQ package: ```nix {{#include rustiq.nix}} @@ -114,13 +126,6 @@ Then import it in the existing `configuration.nix`, preserving any other imports ``` Alternatively, add `./rustiq.nix` to the `modules` list in `nixosSystem`. -See the CPU caveats below before using `native`. To use the standard package, -replace the expression inside the list with: - -```nix -inputs.rustiq.packages.${pkgs.stdenv.hostPlatform.system}.default -``` - From the configuration directory, apply it with `sudo nixos-rebuild switch --flake .#YOUR_HOST` and check `rustiq --version`. Replace `YOUR_HOST` with your NixOS configuration name. See the @@ -128,8 +133,9 @@ Replace `YOUR_HOST` with your NixOS configuration name. See the ## Optional CPU-specific build -The NixOS module above uses this expression. For Home Manager, you can also -replace the standard package entry in `home.packages` with it: +The examples above use the portable standard package. For NixOS or Home +Manager, replace that package expression with this one only when a build tied +to the build machine's CPU is appropriate: ```nix (inputs.rustiq.legacyPackages.${pkgs.stdenv.hostPlatform.system}.cpuTarget "native") @@ -162,6 +168,15 @@ nix build ./result/bin/rustiq --version ``` +For development, clone `develop` explicitly because the default branch is +release-only: + +```sh +git clone --branch develop --single-branch https://github.com/mveril/RustiQ.git +cd RustiQ +nix develop +``` + The package includes the offline manual and shell completions. Run `rustiq docs` after installing, or `./result/bin/rustiq docs` from the checkout. See the [manual instructions](installation.md#consult-the-manual) and the @@ -169,6 +184,6 @@ after installing, or `./result/bin/rustiq docs` from the checkout. See the The package enables online basis downloads but does not bundle sample inputs or basis sets. Download or import a basis before running a calculation; see -[make basis sets available](installation.md#make-basis-sets-available) and the +[obtaining a basis after installation](installation.md#after-installation-obtain-a-basis-and-run-a-calculation) and the [first calculation](first-calculation.md). For development shells, see [Nix setup](../development/nix-setup.md). diff --git a/docs/src/getting-started/installation.md b/docs/src/getting-started/installation.md index 4da57ce8..73f36987 100644 --- a/docs/src/getting-started/installation.md +++ b/docs/src/getting-started/installation.md @@ -1,14 +1,20 @@ # Installation -## Install with Nix +After a release promotion completes, RustiQ is distributed through a prebuilt +archive on GitHub Releases, the Cargo registry, and the Nix flake. The three +publications run independently, so use the channel where the release is listed. +They are all built from the same release tag. -See [installing with Nix](installation-nix.md) for the supported platforms, -profile installation, and CPU-specific builds. +| Method | Choose it when | Platforms and manual | +| --- | --- | --- | +| [GitHub Releases archive](#install-a-prebuilt-release-binary) | You want a runnable executable without installing Rust or Nix. | The six target platforms listed below; includes the offline manual. | +| [Cargo](#install-with-cargo) | You already use Rust and Cargo, or want Cargo to build RustiQ locally. | Native Cargo-supported platforms; uses the online manual. | +| [Nix](installation-nix.md) | You already use Nix or want declarative installation and a pinned package environment. | Linux x86_64/ARM64 and Apple Silicon macOS; includes the offline manual. | ## Install a prebuilt release binary -When a release is available, ordinary users can download the archive for their -operating system and CPU from the [GitHub Releases page](https://github.com/mveril/RustiQ/releases). +Download the archive for your operating system and CPU from the +[GitHub Releases page](https://github.com/mveril/RustiQ/releases). Archives are named `rustiq-chem-.tar.gz` on Unix and `rustiq-chem-.zip` on Windows. Supported targets are `x86_64-unknown-linux-gnu` (Linux Intel/AMD), @@ -51,15 +57,47 @@ Write-Host "SHA-256 verified: $archive" The [release guide](../development/releasing.md#binary-releases) has the full platform-specific verification instructions. For Windows ARM64, replace the archive filename with `rustiq-chem-aarch64-pc-windows-msvc.zip`. Extract the -archive only after the checksum comparison succeeds, then run -`./rustiq --help` from its directory on Linux or macOS, or -`.\rustiq.exe --help` from that directory in PowerShell. To use `rustiq` from -any directory, add the directory containing `rustiq` or `rustiq.exe` to `PATH` -and open a new terminal. +archive only after the checksum comparison succeeds. From the directory holding +the download, extract it with one of the following commands: + +```sh +tar -xzf rustiq-chem-.tar.gz # Linux or macOS +``` + +```powershell +Expand-Archive rustiq-chem-.zip -DestinationPath .\rustiq-chem +``` + +Open a terminal in the extracted directory and run `./rustiq --help` on Linux +or macOS, or `.\rustiq.exe --help` in PowerShell. This works without changing +`PATH`. To run `rustiq` from any directory, add the directory containing +`rustiq` or `rustiq.exe` to `PATH` and open a new terminal. The Cargo package is named `rustiq-chem`; the executable is named `rustiq` (or `rustiq.exe` on Windows). +## Install with Cargo + +Install the released Cargo package from crates.io from any directory. No source +checkout is needed. This builds RustiQ from source and requires Rust and Cargo, +a C/C++ toolchain, and CMake; see [manual setup](../development/manual-setup.md#install-and-build) +for platform prerequisites, but do not follow that page's checkout commands +unless you intend to contribute. + +```sh +cargo install --locked rustiq-chem --version '=0.1.0-beta.1' +rustiq --help +``` + +Ensure Cargo's binary directory is on `PATH`. Cargo-only installations use the +online manual, as described below. + +## Install with Nix + +See [installing with Nix](installation-nix.md) for the supported platforms, +profile and declarative installation, and CPU-specific builds. That page uses +the repository's default branch, `main`, which contains released versions only. + ## Consult the manual Release archives contain an offline HTML manual in `manual/html/` beside the @@ -100,15 +138,16 @@ If browser launching fails, the diagnostic includes the target and the `--print-path` route. RustiQ uses the operating system's default browser opener. PDF export is future work and `--pdf` is not supported. -## Make basis sets available +## After installation: obtain a basis and run a calculation -The archive contains neither sample inputs nor basis data. With network access, -download a basis using `rustiq basis download sto-3g`. This command is available -in online builds and requires network access. To work offline, import a local -Basis Set Exchange JSON file with `rustiq basis import /path/to/basis.json`; -import does not access the network. See the -[basis-set guide](../user-guide/basis-sets.md) and the -[first calculation](first-calculation.md) for a complete binary-only example. +Run `rustiq --version` to confirm the installation. Installed release packages +do not include sample inputs or basis data. With network access, download a +basis using `rustiq basis download sto-3g`. This command is available in online +builds and requires network access. To work offline, import a local Basis Set +Exchange JSON file with `rustiq basis import /path/to/basis.json`; import does +not access the network. The [first calculation](first-calculation.md) works +with an archive, Cargo, or Nix installation. See the +[basis-set guide](../user-guide/basis-sets.md) for basis-data details. ## Build and install from source @@ -118,11 +157,21 @@ parent directory, then run Cargo commands from the checkout root. For complete prerequisite and platform details, see the [manual setup](../development/manual-setup.md#install-and-build). ```sh -git clone https://github.com/mveril/RustiQ.git +git clone --branch develop --single-branch https://github.com/mveril/RustiQ.git cd RustiQ cargo build --locked ``` +The default branch, `main`, is reserved for released versions. Use `develop` +for an active development checkout; a checkout intended to reproduce a release +should instead use its release tag. + +For example, to reproduce the `0.1.0-beta.1` release source, clone its tag: + +```sh +git clone --branch v0.1.0-beta.1 --single-branch https://github.com/mveril/RustiQ.git +``` + `cargo build` does not add the executable to `PATH`. To run without installing, prefix commands with `cargo run --locked --`, for example `cargo run --locked -- --help`. To install the package executable into Cargo's @@ -133,7 +182,5 @@ cargo install --locked --path . rustiq --help ``` -The registry package is also named `rustiq-chem`; install from crates.io only -when the relevant version has been published. See the -[development environments](../development/environments.md) page for Nix, +See the [development environments](../development/environments.md) page for Nix, Docker, and other supported contributor setups. diff --git a/docs/src/getting-started/rustiq.nix b/docs/src/getting-started/rustiq.nix index d02128da..c5edc15c 100644 --- a/docs/src/getting-started/rustiq.nix +++ b/docs/src/getting-started/rustiq.nix @@ -2,6 +2,6 @@ { pkgs, inputs, ... }: { environment.systemPackages = [ - (inputs.rustiq.legacyPackages.${pkgs.stdenv.hostPlatform.system}.cpuTarget "native") + inputs.rustiq.packages.${pkgs.stdenv.hostPlatform.system}.default ]; } diff --git a/docs/src/reference/cli.md b/docs/src/reference/cli.md index 2e1f7188..d353ac39 100644 --- a/docs/src/reference/cli.md +++ b/docs/src/reference/cli.md @@ -75,7 +75,7 @@ For example, the grouped output has this shape (unknown optional fields are omitted and the values depend on the build): ```text -rustiq 0.1.0-alpha.1 +rustiq 0.1.0-beta.1 SOURCE Git commit From dd39cb4f44e91df9a9c9149d0cdd1b9dca1ded2b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20V=C3=A9ril?= Date: Thu, 12 Nov 2026 15:39:12 +0100 Subject: [PATCH 11/12] Harden release metadata and archive checks --- CITATION.cff | 8 +++++--- tools/check-release.py | 3 +++ tools/release_smoke.py | 3 +++ tools/test_prepare_release.py | 8 ++++++++ tools/test_publish_docs.py | 1 + tools/test_release_smoke.py | 18 ++++++++++++++++++ 6 files changed, 38 insertions(+), 3 deletions(-) diff --git a/CITATION.cff b/CITATION.cff index 07cd686d..1928c168 100644 --- a/CITATION.cff +++ b/CITATION.cff @@ -5,7 +5,9 @@ type: software authors: - family-names: Veril given-names: Mickael -version: 0.1.0-alpha.1 -license: MIT OR Apache-2.0 +version: 0.1.0-beta.1 +license: + - MIT + - Apache-2.0 repository-code: https://github.com/mveril/RustiQ -abstract: A Rust prototype for modern quantum chemistry software. +abstract: Experimental quantum chemistry software written in Rust, providing a reusable scientific library and CLI. RustiQ-authored code is available under MIT OR Apache-2.0. diff --git a/tools/check-release.py b/tools/check-release.py index f1cc3b14..9f95ee70 100644 --- a/tools/check-release.py +++ b/tools/check-release.py @@ -29,6 +29,9 @@ def candidate(root): raise ValueError("workspace package versions differ") if manifest["dependencies"]["rustiq-core"]["version"].removeprefix("=") != version: raise ValueError("internal dependency must match release version") + citation = (root / "CITATION.cff").read_text(encoding="utf-8") + if re.findall(r"^version:\s*(\S+)\s*$", citation, re.MULTILINE) != [version]: + raise ValueError("citation version must match release version") return version diff --git a/tools/release_smoke.py b/tools/release_smoke.py index 5c793ce1..cdba11cd 100644 --- a/tools/release_smoke.py +++ b/tools/release_smoke.py @@ -174,6 +174,9 @@ def validate( f"EXTRACTION_FAILURE: expected one rustiq executable in archive, found {len(binaries)}" ) binary = binaries[0] + for name in ("LICENSE-MIT", "LICENSE-APACHE", "COPYING", "DISTRIBUTION.md", "THIRD-PARTY-NOTICES.txt"): + if not any(p.is_file() for p in extracted.rglob(name)): + raise ValueError(f"EXTRACTION_FAILURE: archive lacks {name}") if os.name != "nt": binary.chmod(binary.stat().st_mode | 0o111) clean_path = os.pathsep.join( diff --git a/tools/test_prepare_release.py b/tools/test_prepare_release.py index bc001136..f4f6554d 100644 --- a/tools/test_prepare_release.py +++ b/tools/test_prepare_release.py @@ -12,6 +12,7 @@ def manifests(root, version, core=None, dependency=None): (root / "crates/rustiq-core").mkdir(parents=True) + (root / "CITATION.cff").write_text(f"version: {version}\n") (root / "Cargo.toml").write_text( f'[package]\nversion = "{version}"\n[dependencies]\nrustiq-core = {{version = "{dependency or version}"}}\n' ) @@ -87,3 +88,10 @@ def lookup(url, token=None): monkeypatch.setitem(available.__globals__, "lookup", lookup) with pytest.raises(OSError, match="unavailable"): available("1.2.3", "mveril/RustiQ") + + +def test_rejects_mismatched_citation(tmp_path): + manifests(tmp_path, "1.2.3") + (tmp_path / "CITATION.cff").write_text("version: 1.2.2\n") + with pytest.raises(ValueError, match="citation version"): + candidate(tmp_path) diff --git a/tools/test_publish_docs.py b/tools/test_publish_docs.py index f70d2941..9ade81fb 100644 --- a/tools/test_publish_docs.py +++ b/tools/test_publish_docs.py @@ -225,6 +225,7 @@ def git(*args): (tmp_path / "crates/rustiq-core/Cargo.toml").write_text( f'[package]\nversion = "{release_version}"\n' ) + (tmp_path / "CITATION.cff").write_text(f"version: {release_version}\n") git("add", ".") git("commit", "-m", "Initial main") diff --git a/tools/test_release_smoke.py b/tools/test_release_smoke.py index c00d7bfa..c414e1be 100644 --- a/tools/test_release_smoke.py +++ b/tools/test_release_smoke.py @@ -33,9 +33,13 @@ def setUp(self): "mermaid-init-test.js", ]: (manual / name).write_text("controlled manual asset") + for name in ("LICENSE-MIT", "LICENSE-APACHE", "COPYING", "DISTRIBUTION.md", "THIRD-PARTY-NOTICES.txt"): + (content / name).write_text("document") with tarfile.open(self.archive, "w:gz") as bundle: bundle.add(content / "rustiq", arcname="rustiq") bundle.add(manual, arcname="manual/html") + for name in ("LICENSE-MIT", "LICENSE-APACHE", "COPYING", "DISTRIBUTION.md", "THIRD-PARTY-NOTICES.txt"): + bundle.add(content / name, arcname=name) self.checksum = self.root / "bundle.tar.gz.sha256" self.checksum.write_text( hashlib.sha256(self.archive.read_bytes()).hexdigest() + " bundle.tar.gz\n" @@ -119,6 +123,8 @@ def test_missing_manual_is_rejected(self): binary = self.root / "archive-content/rustiq" with tarfile.open(self.archive, "w:gz") as bundle: bundle.add(binary, arcname="rustiq") + for name in ("LICENSE-MIT", "LICENSE-APACHE", "COPYING", "DISTRIBUTION.md", "THIRD-PARTY-NOTICES.txt"): + bundle.add(binary.parent / name, arcname=name) self.checksum.write_text(hashlib.sha256(self.archive.read_bytes()).hexdigest()) with self.assertRaisesRegex(ValueError, "lacks the complete offline manual"): self.validate() @@ -277,6 +283,18 @@ def test_nonfinite_reference_energy_is_rejected(self): command_runner=self.runner(), ) + def test_missing_distribution_documents(self): + for missing in ("LICENSE-MIT", "LICENSE-APACHE", "COPYING", "DISTRIBUTION.md", "THIRD-PARTY-NOTICES.txt"): + with self.subTest(missing=missing): + content = self.root / "archive-content" + with tarfile.open(self.archive, "w:gz") as bundle: + for path in content.iterdir(): + if path.name != missing: + bundle.add(path, arcname=path.name) + self.checksum.write_text(hashlib.sha256(self.archive.read_bytes()).hexdigest() + " bundle.tar.gz\n") + with self.assertRaisesRegex(ValueError, missing): + self.validate() + def test_success(self): result = self.validate() self.assertEqual(result["checksum"], "verified") From 26ba383e95f6c7d9066168a9831748732f507281 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20V=C3=A9ril?= Date: Thu, 12 Nov 2026 18:49:25 +0100 Subject: [PATCH 12/12] Set development version to 0.1.0-beta.2.dev --- CITATION.cff | 2 +- Cargo.lock | 4 ++-- Cargo.toml | 4 ++-- THIRD-PARTY-NOTICES.sha256 | 2 +- THIRD-PARTY-NOTICES.txt | 10 +++++----- crates/rustiq-core/Cargo.toml | 2 +- 6 files changed, 12 insertions(+), 12 deletions(-) diff --git a/CITATION.cff b/CITATION.cff index 1928c168..d25a8238 100644 --- a/CITATION.cff +++ b/CITATION.cff @@ -5,7 +5,7 @@ type: software authors: - family-names: Veril given-names: Mickael -version: 0.1.0-beta.1 +version: 0.1.0-beta.2.dev license: - MIT - Apache-2.0 diff --git a/Cargo.lock b/Cargo.lock index d2a666e2..27d3d587 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4675,7 +4675,7 @@ dependencies = [ [[package]] name = "rustiq-chem" -version = "0.1.0-beta.1" +version = "0.1.0-beta.2.dev" dependencies = [ "anstream", "anstyle", @@ -4719,7 +4719,7 @@ dependencies = [ [[package]] name = "rustiq-chem-core" -version = "0.1.0-beta.1" +version = "0.1.0-beta.2.dev" dependencies = [ "approx", "bounded-vec-deque", diff --git a/Cargo.toml b/Cargo.toml index 4a3e9125..8d37c391 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -67,7 +67,7 @@ tokio = { version = "1", default-features = false } name = "rustiq-chem" description = "A modern command-line application for quantum chemistry, written in Rust" authors.workspace = true -version = "0.1.0-beta.1" +version = "0.1.0-beta.2.dev" edition.workspace = true license.workspace = true repository.workspace = true @@ -91,7 +91,7 @@ completions = ["dep:clap_complete", "dep:clap_complete_nushell", "dep:sysinfo"] [dependencies] bytesize.workspace = true -rustiq-core = { package = "rustiq-chem-core", path = "crates/rustiq-core", version = "0.1.0-beta.1", default-features = false } +rustiq-core = { package = "rustiq-chem-core", path = "crates/rustiq-core", version = "0.1.0-beta.2.dev", default-features = false } directories = "6" miette = { workspace = true, features = ["fancy"] } clap = { version = "4.6", features = ["cargo", "derive", "env"] } diff --git a/THIRD-PARTY-NOTICES.sha256 b/THIRD-PARTY-NOTICES.sha256 index 7df2b17e..20cff1fa 100644 --- a/THIRD-PARTY-NOTICES.sha256 +++ b/THIRD-PARTY-NOTICES.sha256 @@ -1 +1 @@ -d1447dd924e4c3c979f72ea91ef98554fc96dc7581091097abb5f52193956343 THIRD-PARTY-NOTICES.txt +3f1859908faa6531c31671624f15e6339819bd08c6ba0e70bc6c9cd6305d12d9 THIRD-PARTY-NOTICES.txt diff --git a/THIRD-PARTY-NOTICES.txt b/THIRD-PARTY-NOTICES.txt index e3d1f679..107dec99 100644 --- a/THIRD-PARTY-NOTICES.txt +++ b/THIRD-PARTY-NOTICES.txt @@ -1,6 +1,6 @@ -Cargo.lock SHA-256: 516ff985afb11ee01ab084d5bf4e1e88ab0b52a9517ee03be38757a1d9a34c13 -Notice inputs SHA-256: ece10609b6fe7c51fc664ec00c7614d06eb230fca4ceca36479e1671978f0d8b -Corresponding source: https://github.com/mveril/RustiQ/releases/download/v0.1.0-beta.1/rustiq-corresponding-source.tar.gz +Cargo.lock SHA-256: 2a1954c2ec99b6e87db533afbdd2e3d57caaaec25452083c8310ff2f821730ac +Notice inputs SHA-256: 911b204bcfd6752d237e8ae735612b807bcbe52013a872b01f843c3897c6bc7b +Corresponding source: https://github.com/mveril/RustiQ/releases/download/v0.1.0-beta.2.dev/rustiq-corresponding-source.tar.gz RustiQ third-party licenses ========================== @@ -8220,8 +8220,8 @@ limitations under the License. Apache License 2.0 (Apache-2.0) Used by: - rustiq-chem 0.1.0-beta.1 — https://github.com/mveril/RustiQ - rustiq-chem-core 0.1.0-beta.1 — https://github.com/mveril/RustiQ + rustiq-chem 0.1.0-beta.2.dev — https://github.com/mveril/RustiQ + rustiq-chem-core 0.1.0-beta.2.dev — https://github.com/mveril/RustiQ Apache License Version 2.0, January 2004 diff --git a/crates/rustiq-core/Cargo.toml b/crates/rustiq-core/Cargo.toml index 8fc8fbef..152c0a57 100644 --- a/crates/rustiq-core/Cargo.toml +++ b/crates/rustiq-core/Cargo.toml @@ -2,7 +2,7 @@ name = "rustiq-chem-core" description = "A reusable Rust library for modern quantum chemistry software" authors.workspace = true -version = "0.1.0-beta.1" +version = "0.1.0-beta.2.dev" edition.workspace = true license.workspace = true repository.workspace = true