이 가이드는 깨끗하게 설치한 Topaz에서 패키지 생성, 작성, 정적 검사, 문서화, 직접 오프라인 실행까지 한 번에 진행합니다. 현재 제품 식별자는 Topaz 5.6.5, 언어 모드는 topaz-5.6입니다. 이번 패치는 도구 작업 흐름을 완성하며 v5.6 문법은 바꾸지 않습니다.
준비 사항
Topaz를 설치하고 제품과 언어 식별자를 확인합니다.
curl -fsSL https://topaz.ooo/install.sh | sh
topaz version --verbose
네이티브 빌드에는 Rust 도구 모음이 필요합니다. Python 제품을 실행하려면 Python 3.11 이상이 필요합니다. 패키지를 작성·검사·포맷 검사·테스트·문서화할 때는 이 두 도구가 필요하지 않습니다.
패키지 생성
빈 작업 디렉터리에서 설치된 CLI로 최종 애플리케이션 루트를 만듭니다.
topaz init --root release-inventory
같은 명령을 다시 실행하면 기존 스캐폴드를 덮어쓰지 않고 거부합니다. 완성할 예제의 구조는 다음과 같습니다.
release-inventory/
topaz.toml
src/main.tpz
src/policy.tpz
src/report.tpz
data/releases.csv
data/policy.toml
out/report.txt
registry/
report_slug/1.0.0/topaz.toml
report_slug/1.0.0/src/lib.tpz아래 파일을 작성하기 전에 초기 스캐폴드에 포함되지 않은 디렉터리를 만듭니다.
mkdir -p release-inventory/src release-inventory/data release-inventory/out
mkdir -p registry/report_slug/1.0.0/src
스캐폴드의 매니페스트를 다음 내용으로 바꿉니다. 파일 기능은 data 아래 읽기와 out 아래 쓰기만 허용하며, 일반 경로 탈출과 심볼릭 링크 탈출은 거부합니다.
[package]
name = "release_inventory"
version = "0.1.0"
language = "5.6"
entry = "src/main.tpz"
[build]
target = "native"
deterministic = true
[dependencies]
std = "5.6"
report_slug = "1.0.0"
[capabilities.fs]
read = ["data"]
write = ["out"]
엔트리는 샘플 및 실패 검사 모드를 선택하고, 구조화된 입력을 읽어 보고서를 씁니다.
import src.report { buildReport }
import std.fs
const SAMPLE_CSV = "name,status,score\nCore API,ready,91\nDraft Tool,draft,99\nCLI Pack,ready,84"
const SAMPLE_POLICY = "[policy]\nrequired_status = \"ready\"\nminimum_score = 85"
function renderSample(csv: string) -> Result<int, string> {
let report = buildReport(csv, SAMPLE_POLICY)?
print(report)
Ok(0)
}
function hasArg(args: Array<string>, expected: string) -> bool {
match args.indexOf(expected) {
case Some(_) => true
case None => false
}
}
export function main(args: Array<string>, stdin: string) -> Result<int, string> {
if hasArg(args, "--sample") {
return renderSample(SAMPLE_CSV)
}
if hasArg(args, "--bad-sample") {
return renderSample("name,status,score\nBroken Row,ready,nope")
}
if hasArg(args, "--probe-denied") {
let secret = fs.readText("secret.txt")?
print(secret)
return Ok(0)
}
if hasArg(args, "--probe-symlink") {
let secret = fs.readText("data/escape-link")?
print(secret)
return Ok(0)
}
let inventory = fs.readText("data/releases.csv")?
let policy = fs.readText("data/policy.toml")?
let report = buildReport(inventory, policy)?
fs.writeText("out/report.txt", "{report}\n")?
print(report)
Ok(0)
}src/policy.tpz는 TOML 정책을 검증합니다.
export type Policy = { requiredStatus: string, minimumScore: int }
function field(obj: JSONValue, name: string) -> Result<JSONValue, string> {
match obj.get(name) {
case Some(value) => Ok(value)
case None => Err("missing policy.{name}")
}
}
export function parsePolicy(text: string) -> Result<Policy, string> {
let document = TOML.toJson(TOML.parse(text)?)
let policy = field(document, "policy")?
let requiredValue = field(policy, "required_status")?
let minimumValue = field(policy, "minimum_score")?
let required = requiredValue.asString() ?? ""
let minimum = minimumValue.asInt() ?? -1
if required.byteLength() == 0 {
return Err("policy.required_status must not be empty")
}
if minimum < 0 {
return Err("policy.minimum_score must be a non-negative int")
}
Ok({ requiredStatus: required, minimumScore: minimum })
}src/report.tpz는 로컬 모듈, 컬렉션, CSV 입력, 결정적 오류와 패키지 의존성을 함께 사용합니다.
import report_slug { slug }
import src.policy { Policy, parsePolicy }
function cell(row: Map<string, string>, name: string) -> Result<string, string> {
match row.get(name) {
case Some(value) => Ok(value)
case None => Err("missing CSV column {name}")
}
}
function acceptedLine(row: Map<string, string>, policy: Policy) -> Result<Option<string>, string> {
let name = cell(row, "name")?
let status = cell(row, "status")?
let scoreText = cell(row, "score")?
let score = match toInt(scoreText) {
case Some(value) => value
case None => return Err("invalid score `{scoreText}` for `{name}`")
}
if status != policy.requiredStatus || score < policy.minimumScore {
return Ok(None)
}
Ok(Some("{slug(name)?}:{score}"))
}
export function buildReport(csvText: string, policyText: string) -> Result<string, string> {
let policy = parsePolicy(policyText)?
let rows = CSV.parseWithHeader(csvText)?
let mut accepted: Array<string> = []
for row in rows {
match acceptedLine(row, policy)? {
case Some(line) => accepted.push(line)
case None => ()
}
}
let lines = accepted.sorted()
Ok("accepted={lines.length}\n{lines.join("\n")}")
}로컬 registry 의존성의 매니페스트와 src/lib.tpz를 만듭니다.
[package]
name = "report_slug"
version = "1.0.0"
language = "5.6"
entry = "src/lib.tpz"
[build]
target = "native"
deterministic = true
[dependencies]
std = "5.6"
[exports]
module = "src/lib.tpz"
export function slug(name: string) -> Result<string, string> {
let spaces = Regex.compile(" +")?
Ok(spaces.replaceAll(name.trim(), "-"))
}마지막으로 런타임 입력을 쓰고 출력 디렉터리를 만듭니다.
# data/releases.csv
name,status,score
Core API,ready,91
CLI Pack,ready,84
Python Host,ready,88
Draft Tool,draft,99
# data/policy.toml
[policy]
required_status = "ready"
minimum_score = 85잠금·벤더링·작성 단계 검사
의존성을 vendoring한 뒤 registry를 지웁니다. 이후 패키지 명령은 lockfile과 vendored 바이트만 사용합니다.
topaz vendor --root release-inventory --from registry
rm -rf registry
topaz check --root release-inventory --locked
topaz fmt --check --root release-inventory
topaz test --root release-inventory --locked -- --sample
topaz doc --root release-inventory --locked --out-dir docs-out
fmt --check는 fmt와 같은 포맷터를 호출하지만 아무것도 쓰지 않습니다. 편집기는 topaz lsp --root release-inventory를 시작할 수 있습니다. 저장된 패키지는 vendored 모듈까지 해석하며, 잘못된 미저장 편집은 디스크 파일을 바꾸지 않고 overlay에서 진단됩니다.
검사된 패키지와 결정적인 잘못된 입력 사례를 실행합니다.
topaz run --root release-inventory --locked
topaz run --root release-inventory --locked -- --bad-sample
정상 보고서는 다음과 같습니다.
accepted=2
Core-API:91
Python-Host:88두 제품 빌드
네이티브와 Python 제품은 서로 다른 명령을 사용합니다.
topaz build --release --root release-inventory --locked --out-dir native-out
topaz build --target python --root release-inventory --locked --out-dir python-out
네이티브 런타임 산출물은 native-out/target/release/program이며 Windows에서는 program.exe입니다. Python 런타임 산출물은 python-out/program.py와 python-out/topaz_py_rt.py입니다. 각 빌드는 5.6.5 도구 모음, topaz-5.6 언어 모드, 타겟, 런타임 요구 사항과 관리 파일 해시를 기록한 topaz-artifact.json도 만듭니다.
런타임 산출물과 data/, 쓰기 가능한 빈 out/만 별도의 런타임 디렉터리로 복사합니다. 그다음 애플리케이션 소스, registry, 생성 문서와 두 빌드 디렉터리를 삭제합니다.
mkdir -p native-runtime/data native-runtime/out
mkdir -p python-runtime/data python-runtime/out
cp native-out/target/release/program native-runtime/program
cp python-out/program.py python-out/topaz_py_rt.py python-runtime/
cp release-inventory/data/* native-runtime/data/
cp release-inventory/data/* python-runtime/data/
rm -rf release-inventory registry docs-out native-out python-out
선언한 상대 data/와 out/ 루트가 유지되도록 네트워크와 proxy를 차단하고 각 제품을 자체 런타임 디렉터리에서 실행합니다.
(cd native-runtime && env HTTP_PROXY=http://127.0.0.1:9 HTTPS_PROXY=http://127.0.0.1:9 ALL_PROXY=http://127.0.0.1:9 NO_PROXY= CARGO_NET_OFFLINE=true ./program)
(cd python-runtime && env HTTP_PROXY=http://127.0.0.1:9 HTTPS_PROXY=http://127.0.0.1:9 ALL_PROXY=http://127.0.0.1:9 NO_PROXY= CARGO_NET_OFFLINE=true python3 program.py)
두 제품은 Topaz checkout, 애플리케이션 소스, registry, Cargo 빌드 트리나 네트워크 없이 같은 보고서와 출력 파일을 만듭니다. 네이티브 제품에는 실행 파일과 선언한 런타임 데이터만 필요합니다. Python 제품에는 topaz_py_rt.py와 Python 3.11 이상도 필요합니다.