Topazdocs
시작

애플리케이션 루프

설치된 Topaz로 다중 모듈 패키지를 만들고 소스 없이 실행되는 네이티브·Python 제품까지 완성합니다.

이 가이드는 깨끗하게 설치한 Topaz에서 패키지 생성, 작성, 정적 검사, 문서화, 직접 오프라인 실행까지 한 번에 진행합니다. 현재 제품 식별자는 Topaz 5.6.5, 언어 모드는 topaz-5.6입니다. 이번 패치는 도구 작업 흐름을 완성하며 v5.6 문법은 바꾸지 않습니다.

준비 사항

Topaz를 설치하고 제품과 언어 식별자를 확인합니다.

BASH
curl -fsSL https://topaz.ooo/install.sh | sh
topaz version --verbose

네이티브 빌드에는 Rust 도구 모음이 필요합니다. Python 제품을 실행하려면 Python 3.11 이상이 필요합니다. 패키지를 작성·검사·포맷 검사·테스트·문서화할 때는 이 두 도구가 필요하지 않습니다.

패키지 생성

빈 작업 디렉터리에서 설치된 CLI로 최종 애플리케이션 루트를 만듭니다.

BASH
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

아래 파일을 작성하기 전에 초기 스캐폴드에 포함되지 않은 디렉터리를 만듭니다.

BASH
mkdir -p release-inventory/src release-inventory/data release-inventory/out
mkdir -p registry/report_slug/1.0.0/src

스캐폴드의 매니페스트를 다음 내용으로 바꿉니다. 파일 기능은 data 아래 읽기와 out 아래 쓰기만 허용하며, 일반 경로 탈출과 심볼릭 링크 탈출은 거부합니다.

TOML
[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"]

엔트리는 샘플 및 실패 검사 모드를 선택하고, 구조화된 입력을 읽어 보고서를 씁니다.

TOPAZ
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 정책을 검증합니다.

TOPAZ
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 입력, 결정적 오류와 패키지 의존성을 함께 사용합니다.

TOPAZ
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를 만듭니다.

TOML
[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"
TOPAZ
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 바이트만 사용합니다.

BASH
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 --checkfmt와 같은 포맷터를 호출하지만 아무것도 쓰지 않습니다. 편집기는 topaz lsp --root release-inventory를 시작할 수 있습니다. 저장된 패키지는 vendored 모듈까지 해석하며, 잘못된 미저장 편집은 디스크 파일을 바꾸지 않고 overlay에서 진단됩니다.

검사된 패키지와 결정적인 잘못된 입력 사례를 실행합니다.

BASH
topaz run --root release-inventory --locked
topaz run --root release-inventory --locked -- --bad-sample

정상 보고서는 다음과 같습니다.

accepted=2
Core-API:91
Python-Host:88

두 제품 빌드

네이티브와 Python 제품은 서로 다른 명령을 사용합니다.

BASH
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.pypython-out/topaz_py_rt.py입니다. 각 빌드는 5.6.5 도구 모음, topaz-5.6 언어 모드, 타겟, 런타임 요구 사항과 관리 파일 해시를 기록한 topaz-artifact.json도 만듭니다.

런타임 산출물과 data/, 쓰기 가능한 빈 out/만 별도의 런타임 디렉터리로 복사합니다. 그다음 애플리케이션 소스, registry, 생성 문서와 두 빌드 디렉터리를 삭제합니다.

BASH
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를 차단하고 각 제품을 자체 런타임 디렉터리에서 실행합니다.

BASH
(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 이상도 필요합니다.

관련 문서