incan 0.4.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
incan-0.4.0/.gitignore ADDED
@@ -0,0 +1,65 @@
1
+ # Generated by Cargo
2
+ # will have compiled files and executables
3
+ debug
4
+ target
5
+
6
+ # These are backup files generated by rustfmt
7
+ **/*.rs.bk
8
+
9
+ # MSVC Windows builds of rustc generate these, which store debugging information
10
+ *.pdb
11
+
12
+ # Generated by cargo mutants
13
+ # Contains mutation testing data
14
+ **/mutants.out*/
15
+
16
+ # Python (from any scripts/tooling that might be run)
17
+ __pycache__/
18
+ *.py[cod]
19
+ *$py.class
20
+ .mypy_cache/
21
+ .pytest_cache/
22
+ .ruff_cache/
23
+ *.egg-info/
24
+ .venv/
25
+ venv/
26
+ .env/
27
+ env/
28
+ dist/
29
+ build/
30
+
31
+ # Node.js / VS Code extension (editors/vscode)
32
+ node_modules/
33
+ out/
34
+ *.vsix
35
+ .vscode-test/
36
+ *.tsbuildinfo
37
+
38
+ # npm / npx caches and logs (local tooling)
39
+ .npm-cache/
40
+ npm-debug.log*
41
+ yarn-debug.log*
42
+ yarn-error.log*
43
+ pnpm-debug.log*
44
+
45
+ # Local AI agent instructions
46
+ .claude/
47
+ .agents/state/*
48
+ !.agents/state/.gitkeep
49
+
50
+ # MkDocs build output
51
+ site/
52
+
53
+ # Documentation engines
54
+ .obsidian/
55
+
56
+ # -- Project-specific files --
57
+ examples/advanced/test_output.txt
58
+
59
+ # OrdinalMap benchmark local outputs
60
+ benchmarks/collections/ordinal_map/__pycache__/
61
+ benchmarks/collections/ordinal_map/target/
62
+
63
+ # Generated files we do not want to commit
64
+ workspaces/docs-site/docs/_snippets/rfcs_refs.md
65
+ workspaces/docs-site/docs/_snippets/tables/rfcs_index.md
incan-0.4.0/PKG-INFO ADDED
@@ -0,0 +1,31 @@
1
+ Metadata-Version: 2.4
2
+ Name: incan
3
+ Version: 0.4.0
4
+ Summary: Thin installer package for the Incan toolchain
5
+ Project-URL: Homepage, https://incan.io
6
+ Project-URL: Repository, https://github.com/encero-systems/incan
7
+ Author-email: Danny Meijer <dannys.code.corner@gmail.com>
8
+ License-Expression: Apache-2.0
9
+ Keywords: compiler,incan,toolchain
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Environment :: Console
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Operating System :: MacOS
14
+ Classifier: Operating System :: POSIX :: Linux
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Topic :: Software Development :: Compilers
17
+ Requires-Python: >=3.9
18
+ Description-Content-Type: text/markdown
19
+
20
+ # Incan Toolchain Python Installer
21
+
22
+ Incan is a statically typed, Pythonic programming language for writing clear high-level application code that compiles to native Rust. Learn more at [incan.io](https://incan.io).
23
+
24
+ This package is a thin installer and command shim for the Incan toolchain. It installs verified toolchain archives from the shared Incan release manifest instead of building the compiler from Python packaging.
25
+
26
+ ```bash
27
+ pipx install incan
28
+ incan --version
29
+ ```
30
+
31
+ The command shims install the toolchain into a package-local cache on first use and default to the release manifest that matches this package version. If `pipx` warns that `~/.local/bin` is not on `PATH`, run `pipx ensurepath` or add that directory to your shell startup before calling `incan`. Set `INCAN_PIP_TOOLCHAIN_HOME`, `INCAN_PIP_BIN_DIR`, or `INCAN_TOOLCHAIN_MANIFEST` when you need a custom cache location or manifest.
incan-0.4.0/README.md ADDED
@@ -0,0 +1,12 @@
1
+ # Incan Toolchain Python Installer
2
+
3
+ Incan is a statically typed, Pythonic programming language for writing clear high-level application code that compiles to native Rust. Learn more at [incan.io](https://incan.io).
4
+
5
+ This package is a thin installer and command shim for the Incan toolchain. It installs verified toolchain archives from the shared Incan release manifest instead of building the compiler from Python packaging.
6
+
7
+ ```bash
8
+ pipx install incan
9
+ incan --version
10
+ ```
11
+
12
+ The command shims install the toolchain into a package-local cache on first use and default to the release manifest that matches this package version. If `pipx` warns that `~/.local/bin` is not on `PATH`, run `pipx ensurepath` or add that directory to your shell startup before calling `incan`. Set `INCAN_PIP_TOOLCHAIN_HOME`, `INCAN_PIP_BIN_DIR`, or `INCAN_TOOLCHAIN_MANIFEST` when you need a custom cache location or manifest.
@@ -0,0 +1,36 @@
1
+ [build-system]
2
+ requires = ["hatchling>=1.25"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "incan"
7
+ version = "0.4.0"
8
+ description = "Thin installer package for the Incan toolchain"
9
+ readme = "README.md"
10
+ requires-python = ">=3.9"
11
+ license = "Apache-2.0"
12
+ authors = [
13
+ { name = "Danny Meijer", email = "dannys.code.corner@gmail.com" },
14
+ ]
15
+ keywords = ["incan", "compiler", "toolchain"]
16
+ classifiers = [
17
+ "Development Status :: 3 - Alpha",
18
+ "Environment :: Console",
19
+ "Intended Audience :: Developers",
20
+ "Operating System :: MacOS",
21
+ "Operating System :: POSIX :: Linux",
22
+ "Programming Language :: Python :: 3",
23
+ "Topic :: Software Development :: Compilers",
24
+ ]
25
+
26
+ [project.urls]
27
+ Homepage = "https://incan.io"
28
+ Repository = "https://github.com/encero-systems/incan"
29
+
30
+ [project.scripts]
31
+ incan = "incan_toolchain.cli:incan"
32
+ incan-lsp = "incan_toolchain.cli:incan_lsp"
33
+ install-incan = "incan_toolchain.cli:install"
34
+
35
+ [tool.hatch.build.targets.wheel]
36
+ packages = ["src/incan_toolchain"]
@@ -0,0 +1,4 @@
1
+ """Thin Python installer package for the Incan toolchain."""
2
+
3
+ __all__ = ["__version__"]
4
+ __version__ = "0.4.0"
@@ -0,0 +1,114 @@
1
+ """Command shims for the Incan toolchain Python package."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ import re
7
+ import subprocess
8
+ import sys
9
+ from pathlib import Path
10
+
11
+
12
+ def _package_root() -> Path:
13
+ return Path(__file__).resolve().parents[2]
14
+
15
+
16
+ def _installer_script() -> Path:
17
+ candidates = [
18
+ Path(__file__).resolve().parent / "vendor" / "install-incan.sh",
19
+ _package_root().parent / "install-incan.sh",
20
+ ]
21
+ for candidate in candidates:
22
+ if candidate.exists():
23
+ return candidate
24
+ raise RuntimeError("could not find bundled install-incan.sh")
25
+
26
+
27
+ def _toolchain_home() -> Path:
28
+ return Path(os.environ.get("INCAN_PIP_TOOLCHAIN_HOME", _package_root() / ".incan" / "home"))
29
+
30
+
31
+ def _bin_dir() -> Path:
32
+ return Path(os.environ.get("INCAN_PIP_BIN_DIR", _package_root() / ".incan" / "bin"))
33
+
34
+
35
+ def _package_version() -> str:
36
+ init_text = (Path(__file__).resolve().parent / "__init__.py").read_text(encoding="utf-8")
37
+ match = re.search(r'__version__\s*=\s*"([^"]+)"', init_text)
38
+ if not match:
39
+ raise RuntimeError("could not read incan package version")
40
+ return match.group(1)
41
+
42
+
43
+ def _release_tag() -> str:
44
+ version = _package_version()
45
+ match = re.fullmatch(r"(?P<base>\d+\.\d+\.\d+)(?P<kind>a|b|rc)(?P<number>\d+)", version)
46
+ if match:
47
+ return f"v{match.group('base')}-{match.group('kind')}{match.group('number')}"
48
+ match = re.fullmatch(r"(?P<base>\d+\.\d+\.\d+)\.dev(?P<number>\d+)", version)
49
+ if match:
50
+ return f"v{match.group('base')}-dev.{match.group('number')}"
51
+ return f"v{version}"
52
+
53
+
54
+ def _package_manifest_url() -> str:
55
+ release = _release_tag()
56
+ return f"https://github.com/encero-systems/incan/releases/download/{release}/manifest.json"
57
+
58
+
59
+ def _has_value_option(args: list[str], name: str) -> bool:
60
+ return name in args or any(arg.startswith(f"{name}=") for arg in args)
61
+
62
+
63
+ def _installer_args(args: list[str]) -> list[str]:
64
+ next_args = list(args)
65
+ if not _has_value_option(next_args, "--manifest") and not os.environ.get("INCAN_TOOLCHAIN_MANIFEST"):
66
+ next_args.extend(["--manifest", _package_manifest_url()])
67
+ if not _has_value_option(next_args, "--incan-home"):
68
+ next_args.extend(["--incan-home", str(_toolchain_home())])
69
+ if not _has_value_option(next_args, "--bin-dir"):
70
+ next_args.extend(["--bin-dir", str(_bin_dir())])
71
+ return next_args
72
+
73
+
74
+ def _run_installer(args: list[str]) -> int:
75
+ return subprocess.call(["bash", str(_installer_script()), *_installer_args(args)])
76
+
77
+
78
+ def install() -> None:
79
+ raise SystemExit(_run_installer(sys.argv[1:]))
80
+
81
+
82
+ def _command_path(command: str) -> Path:
83
+ return _bin_dir() / command
84
+
85
+
86
+ def _ensure_command(command: str) -> None:
87
+ if _command_path(command).exists():
88
+ return
89
+ status = _run_installer([])
90
+ if status != 0:
91
+ raise SystemExit(status)
92
+
93
+
94
+ def _run_command(command: str) -> None:
95
+ _ensure_command(command)
96
+ os.execv(str(_command_path(command)), [command, *sys.argv[1:]])
97
+
98
+
99
+ def incan() -> None:
100
+ _run_command("incan")
101
+
102
+
103
+ def incan_lsp() -> None:
104
+ _run_command("incan-lsp")
105
+
106
+
107
+ if __name__ == "__main__":
108
+ if len(sys.argv) >= 2 and sys.argv[1] == "install":
109
+ sys.argv.pop(1)
110
+ install()
111
+ if len(sys.argv) >= 2 and sys.argv[1] == "incan-lsp":
112
+ sys.argv.pop(1)
113
+ incan_lsp()
114
+ incan()
@@ -0,0 +1,432 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ default_manifest_url="https://github.com/encero-systems/incan/releases/latest/download/manifest.json"
5
+ manifest_ref="${INCAN_TOOLCHAIN_MANIFEST:-$default_manifest_url}"
6
+ incan_home="${INCAN_HOME:-$HOME/.incan}"
7
+ bin_dir="${INCAN_BIN_DIR:-$HOME/.local/bin}"
8
+ skip_rust_install="${INCAN_SKIP_RUST_INSTALL:-false}"
9
+ rustup_init_ref="${INCAN_RUSTUP_INIT:-https://sh.rustup.rs}"
10
+ target_override=""
11
+ archive_override=""
12
+ dry_run="false"
13
+
14
+ usage() {
15
+ cat <<'USAGE'
16
+ Install the Incan toolchain from a versioned release manifest.
17
+
18
+ Usage:
19
+ install-incan.sh [options]
20
+
21
+ Options:
22
+ --manifest <URL|PATH> Release manifest to use (default: GitHub Releases latest manifest)
23
+ --target <TRIPLE> Host target override for tests or cross-install staging
24
+ --archive <PATH> Use an already-downloaded archive while still verifying the manifest checksum
25
+ --incan-home <PATH> toolchain install root (default: $INCAN_HOME or ~/.incan)
26
+ --bin-dir <PATH> Directory where command symlinks are created (default: $INCAN_BIN_DIR or ~/.local/bin)
27
+ --skip-rust Do not install or update the Rust backend toolchain
28
+ --dry-run Resolve manifest and target without downloading, extracting, or writing files
29
+ -h, --help Show this help
30
+ USAGE
31
+ }
32
+
33
+ fail() {
34
+ printf 'install-incan: %s\n' "$*" >&2
35
+ exit 1
36
+ }
37
+
38
+ truthy() {
39
+ case "${1:-}" in
40
+ 1|true|TRUE|yes|YES|on|ON) return 0 ;;
41
+ *) return 1 ;;
42
+ esac
43
+ }
44
+
45
+ has_command() {
46
+ command -v "$1" >/dev/null 2>&1
47
+ }
48
+
49
+ while [ "$#" -gt 0 ]; do
50
+ case "$1" in
51
+ --manifest)
52
+ [ "$#" -ge 2 ] || fail "--manifest requires a value"
53
+ manifest_ref="$2"
54
+ shift 2
55
+ ;;
56
+ --target)
57
+ [ "$#" -ge 2 ] || fail "--target requires a value"
58
+ target_override="$2"
59
+ shift 2
60
+ ;;
61
+ --archive)
62
+ [ "$#" -ge 2 ] || fail "--archive requires a value"
63
+ archive_override="$2"
64
+ shift 2
65
+ ;;
66
+ --incan-home)
67
+ [ "$#" -ge 2 ] || fail "--incan-home requires a value"
68
+ incan_home="$2"
69
+ shift 2
70
+ ;;
71
+ --bin-dir)
72
+ [ "$#" -ge 2 ] || fail "--bin-dir requires a value"
73
+ bin_dir="$2"
74
+ shift 2
75
+ ;;
76
+ --skip-rust)
77
+ skip_rust_install="true"
78
+ shift
79
+ ;;
80
+ --dry-run)
81
+ dry_run="true"
82
+ shift
83
+ ;;
84
+ -h|--help)
85
+ usage
86
+ exit 0
87
+ ;;
88
+ *)
89
+ fail "unknown option: $1"
90
+ ;;
91
+ esac
92
+ done
93
+
94
+ require_command() {
95
+ has_command "$1" || fail "required command not found: $1"
96
+ }
97
+
98
+ detect_target() {
99
+ local os arch
100
+ os="$(uname -s)"
101
+ arch="$(uname -m)"
102
+ case "${os}:${arch}" in
103
+ Darwin:arm64|Darwin:aarch64) printf '%s\n' "aarch64-apple-darwin" ;;
104
+ Darwin:x86_64) printf '%s\n' "x86_64-apple-darwin" ;;
105
+ Linux:x86_64|Linux:amd64) printf '%s\n' "x86_64-unknown-linux-gnu" ;;
106
+ Linux:arm64|Linux:aarch64) fail "Linux arm64 toolchain archives are not shipped in 0.4; use a source build or x86_64 Linux host for now" ;;
107
+ MINGW*|MSYS*|CYGWIN*|Windows_NT:*) fail "native Windows is not supported by the 0.4 toolchain installer; use WSL2 for now" ;;
108
+ *) fail "unsupported host: ${os} ${arch}" ;;
109
+ esac
110
+ }
111
+
112
+ copy_or_download() {
113
+ local ref="$1"
114
+ local out="$2"
115
+ case "$ref" in
116
+ http://*|https://*)
117
+ require_command curl
118
+ curl -fL --progress-bar "$ref" -o "$out"
119
+ ;;
120
+ file://*)
121
+ cp "${ref#file://}" "$out"
122
+ ;;
123
+ *)
124
+ cp "$ref" "$out"
125
+ ;;
126
+ esac
127
+ }
128
+
129
+ json_top_value() {
130
+ local file="$1"
131
+ local field="$2"
132
+ if command -v jq >/dev/null 2>&1; then
133
+ jq -r --arg field "$field" '.[$field] // empty' "$file"
134
+ else
135
+ require_command python3
136
+ python3 - "$file" "$field" <<'PY'
137
+ import json
138
+ import sys
139
+
140
+ with open(sys.argv[1], encoding="utf-8") as handle:
141
+ payload = json.load(handle)
142
+ value = payload.get(sys.argv[2], "")
143
+ if isinstance(value, (dict, list)):
144
+ print(json.dumps(value, separators=(",", ":")))
145
+ else:
146
+ print(value)
147
+ PY
148
+ fi
149
+ }
150
+
151
+ json_host_value() {
152
+ local file="$1"
153
+ local target="$2"
154
+ local field="$3"
155
+ if command -v jq >/dev/null 2>&1; then
156
+ jq -r --arg target "$target" --arg field "$field" '.hosts[$target][$field] // empty' "$file"
157
+ else
158
+ require_command python3
159
+ python3 - "$file" "$target" "$field" <<'PY'
160
+ import json
161
+ import sys
162
+
163
+ with open(sys.argv[1], encoding="utf-8") as handle:
164
+ payload = json.load(handle)
165
+ value = payload.get("hosts", {}).get(sys.argv[2], {}).get(sys.argv[3], "")
166
+ if isinstance(value, (dict, list)):
167
+ print(json.dumps(value, separators=(",", ":")))
168
+ else:
169
+ print(value)
170
+ PY
171
+ fi
172
+ }
173
+
174
+ json_command_path() {
175
+ local file="$1"
176
+ local target="$2"
177
+ local command_name="$3"
178
+ if command -v jq >/dev/null 2>&1; then
179
+ jq -r --arg target "$target" --arg command "$command_name" '.hosts[$target].commands[$command] // empty' "$file"
180
+ else
181
+ require_command python3
182
+ python3 - "$file" "$target" "$command_name" <<'PY'
183
+ import json
184
+ import sys
185
+
186
+ with open(sys.argv[1], encoding="utf-8") as handle:
187
+ payload = json.load(handle)
188
+ print(payload.get("hosts", {}).get(sys.argv[2], {}).get("commands", {}).get(sys.argv[3], ""))
189
+ PY
190
+ fi
191
+ }
192
+
193
+ json_commands() {
194
+ local file="$1"
195
+ if command -v jq >/dev/null 2>&1; then
196
+ jq -r '.commands[]?' "$file"
197
+ else
198
+ require_command python3
199
+ python3 - "$file" <<'PY'
200
+ import json
201
+ import sys
202
+
203
+ with open(sys.argv[1], encoding="utf-8") as handle:
204
+ payload = json.load(handle)
205
+ for command_name in payload.get("commands", []):
206
+ print(command_name)
207
+ PY
208
+ fi
209
+ }
210
+
211
+ json_hosts() {
212
+ local file="$1"
213
+ if command -v jq >/dev/null 2>&1; then
214
+ jq -r '.hosts | keys[]?' "$file"
215
+ else
216
+ require_command python3
217
+ python3 - "$file" <<'PY'
218
+ import json
219
+ import sys
220
+
221
+ with open(sys.argv[1], encoding="utf-8") as handle:
222
+ payload = json.load(handle)
223
+ for host in payload.get("hosts", {}):
224
+ print(host)
225
+ PY
226
+ fi
227
+ }
228
+
229
+ json_rust_toolchain_value() {
230
+ local file="$1"
231
+ local field="$2"
232
+ if command -v jq >/dev/null 2>&1; then
233
+ jq -r --arg field "$field" '.rust_toolchain[$field] // empty' "$file"
234
+ else
235
+ require_command python3
236
+ python3 - "$file" "$field" <<'PY'
237
+ import json
238
+ import sys
239
+
240
+ with open(sys.argv[1], encoding="utf-8") as handle:
241
+ payload = json.load(handle)
242
+ value = payload.get("rust_toolchain", {}).get(sys.argv[2], "")
243
+ if isinstance(value, (dict, list)):
244
+ print(json.dumps(value, separators=(",", ":")))
245
+ else:
246
+ print(value)
247
+ PY
248
+ fi
249
+ }
250
+
251
+ json_rust_targets() {
252
+ local file="$1"
253
+ if command -v jq >/dev/null 2>&1; then
254
+ jq -r '.rust_toolchain.targets[]?' "$file"
255
+ else
256
+ require_command python3
257
+ python3 - "$file" <<'PY'
258
+ import json
259
+ import sys
260
+
261
+ with open(sys.argv[1], encoding="utf-8") as handle:
262
+ payload = json.load(handle)
263
+ for target in payload.get("rust_toolchain", {}).get("targets", []):
264
+ print(target)
265
+ PY
266
+ fi
267
+ }
268
+
269
+ sha256_file() {
270
+ local file="$1"
271
+ if command -v sha256sum >/dev/null 2>&1; then
272
+ sha256sum "$file" | awk '{print $1}'
273
+ elif command -v shasum >/dev/null 2>&1; then
274
+ shasum -a 256 "$file" | awk '{print $1}'
275
+ else
276
+ fail "required command not found: sha256sum or shasum"
277
+ fi
278
+ }
279
+
280
+ install_rustup() {
281
+ local channel="$1"
282
+ printf 'Installing Rust backend with rustup (%s)...\n' "$channel"
283
+ case "$rustup_init_ref" in
284
+ http://*|https://*)
285
+ require_command curl
286
+ curl --proto '=https' --tlsv1.2 -sSf "$rustup_init_ref" | sh -s -- -y --profile minimal --default-toolchain "$channel"
287
+ ;;
288
+ file://*)
289
+ sh "${rustup_init_ref#file://}" -y --profile minimal --default-toolchain "$channel"
290
+ ;;
291
+ *)
292
+ sh "$rustup_init_ref" -y --profile minimal --default-toolchain "$channel"
293
+ ;;
294
+ esac
295
+ export PATH="${CARGO_HOME:-$HOME/.cargo}/bin:$PATH"
296
+ }
297
+
298
+ ensure_rust_backend() {
299
+ local manifest_file="$1"
300
+ local channel
301
+ channel="$(json_rust_toolchain_value "$manifest_file" "channel")"
302
+ [ -n "$channel" ] || channel="stable"
303
+
304
+ if truthy "$skip_rust_install"; then
305
+ printf 'Rust backend provisioning skipped.\n'
306
+ return 0
307
+ fi
308
+
309
+ if ! has_command rustup; then
310
+ install_rustup "$channel"
311
+ fi
312
+ has_command rustup || fail "rustup was not available after Rust backend installation"
313
+
314
+ if ! has_command cargo || ! has_command rustc; then
315
+ printf 'Installing Rust toolchain %s...\n' "$channel"
316
+ rustup toolchain install "$channel" --profile minimal
317
+ rustup default "$channel"
318
+ export PATH="${CARGO_HOME:-$HOME/.cargo}/bin:$PATH"
319
+ fi
320
+ has_command cargo || fail "cargo was not available after Rust backend installation"
321
+ has_command rustc || fail "rustc was not available after Rust backend installation"
322
+
323
+ printf 'Rust backend:\n'
324
+ printf ' rustc: %s\n' "$(rustc --version)"
325
+ printf ' cargo: %s\n' "$(cargo --version)"
326
+ while IFS= read -r rust_target; do
327
+ [ -n "$rust_target" ] || continue
328
+ printf ' target: %s\n' "$rust_target"
329
+ rustup target add "$rust_target"
330
+ done <<RUST_TARGETS
331
+ $(json_rust_targets "$manifest_file")
332
+ RUST_TARGETS
333
+ }
334
+
335
+ target="${target_override:-$(detect_target)}"
336
+ tmp_dir="$(mktemp -d)"
337
+ trap 'rm -rf "$tmp_dir"' EXIT
338
+ manifest_file="${tmp_dir}/manifest.json"
339
+ copy_or_download "$manifest_ref" "$manifest_file"
340
+
341
+ schema_version="$(json_top_value "$manifest_file" "schema_version")"
342
+ [ "$schema_version" = "1" ] || fail "unsupported toolchain manifest schema_version: ${schema_version:-missing}"
343
+
344
+ toolchain_version="$(json_top_value "$manifest_file" "toolchain_version")"
345
+ [ -n "$toolchain_version" ] || fail "manifest is missing toolchain_version"
346
+
347
+ archive_url="$(json_host_value "$manifest_file" "$target" "archive_url")"
348
+ archive_sha256="$(json_host_value "$manifest_file" "$target" "archive_sha256")"
349
+ archive_format="$(json_host_value "$manifest_file" "$target" "archive_format")"
350
+ if [ -z "$archive_url" ] || [ -z "$archive_sha256" ]; then
351
+ printf 'Available targets:\n' >&2
352
+ json_hosts "$manifest_file" >&2
353
+ fail "manifest does not contain an archive for target ${target}"
354
+ fi
355
+ [ "${archive_format:-tar.gz}" = "tar.gz" ] || fail "unsupported archive format for ${target}: ${archive_format}"
356
+
357
+ printf 'Incan toolchain %s\n' "$toolchain_version"
358
+ printf ' target: %s\n' "$target"
359
+ printf ' archive: %s\n' "$archive_url"
360
+ printf ' incan home: %s\n' "$incan_home"
361
+ printf ' bin dir: %s\n' "$bin_dir"
362
+
363
+ if [ "$dry_run" = "true" ]; then
364
+ printf 'Dry run only; no files were written.\n'
365
+ exit 0
366
+ fi
367
+
368
+ ensure_rust_backend "$manifest_file"
369
+ require_command tar
370
+ archive_file="${archive_override:-${tmp_dir}/toolchain.tar.gz}"
371
+ if [ -n "$archive_override" ]; then
372
+ [ -f "$archive_override" ] || fail "archive override does not exist: $archive_override"
373
+ else
374
+ printf 'Downloading toolchain archive...\n'
375
+ copy_or_download "$archive_url" "$archive_file"
376
+ fi
377
+
378
+ printf 'Verifying toolchain archive checksum...\n'
379
+ actual_sha256="$(sha256_file "$archive_file")"
380
+ [ "$actual_sha256" = "$archive_sha256" ] || fail "checksum mismatch for ${archive_file}: expected ${archive_sha256}, got ${actual_sha256}"
381
+
382
+ toolchain_dir="${incan_home}/toolchains/${toolchain_version}"
383
+ if [ -e "$toolchain_dir" ]; then
384
+ fail "toolchain directory already exists: ${toolchain_dir}"
385
+ fi
386
+ extract_dir="${tmp_dir}/toolchain"
387
+ mkdir -p "$extract_dir"
388
+ printf 'Extracting toolchain archive...\n'
389
+ tar -xzf "$archive_file" -C "$extract_dir"
390
+
391
+ while IFS= read -r command_name; do
392
+ [ -n "$command_name" ] || continue
393
+ command_path="$(json_command_path "$manifest_file" "$target" "$command_name")"
394
+ [ -n "$command_path" ] || command_path="bin/${command_name}"
395
+ source_path="${extract_dir}/${command_path}"
396
+ [ -f "$source_path" ] || fail "archive did not contain ${command_name} at ${command_path}"
397
+ done <<COMMANDS
398
+ $(json_commands "$manifest_file")
399
+ COMMANDS
400
+
401
+ [ -f "${extract_dir}/stdlib/testing.incn" ] || fail "archive did not contain stdlib/testing.incn"
402
+ for support_crate in incan_core incan_derive incan_stdlib incan_web_macros; do
403
+ [ -f "${extract_dir}/crates/${support_crate}/Cargo.toml" ] || fail "archive did not contain crates/${support_crate}/Cargo.toml"
404
+ done
405
+ [ -f "${extract_dir}/crates/Cargo.toml" ] || fail "archive did not contain crates/Cargo.toml"
406
+
407
+ mkdir -p "$(dirname "$toolchain_dir")" "$bin_dir"
408
+ mv "$extract_dir" "$toolchain_dir"
409
+
410
+ while IFS= read -r command_name; do
411
+ [ -n "$command_name" ] || continue
412
+ command_path="$(json_command_path "$manifest_file" "$target" "$command_name")"
413
+ [ -n "$command_path" ] || command_path="bin/${command_name}"
414
+ source_path="${toolchain_dir}/${command_path}"
415
+ chmod +x "$source_path"
416
+ link_path="${bin_dir}/${command_name}"
417
+ if [ -e "$link_path" ] && [ ! -L "$link_path" ]; then
418
+ fail "refusing to replace non-symlink command path: ${link_path}"
419
+ fi
420
+ ln -sfn "$source_path" "$link_path"
421
+ printf 'Linked %s -> %s\n' "$link_path" "$source_path"
422
+ done <<COMMANDS
423
+ $(json_commands "$manifest_file")
424
+ COMMANDS
425
+
426
+ current_link="${incan_home}/current"
427
+ if [ -e "$current_link" ] && [ ! -L "$current_link" ]; then
428
+ fail "refusing to replace non-symlink current toolchain path: ${current_link}"
429
+ fi
430
+ ln -sfn "$toolchain_dir" "$current_link"
431
+
432
+ printf 'Installed Incan toolchain %s into %s\n' "$toolchain_version" "$toolchain_dir"