패키지 매니저

Package Manager Design

Overview

This document describes the design of the Vais package manager (vais pkg), which provides dependency management through a vais.toml configuration file.

Goals

  1. Simple Configuration: Easy-to-understand vais.toml format
  2. Local Dependencies: Support for local path dependencies during development
  3. Version Management: Semantic versioning for packages
  4. Reproducible Builds: Lock file for deterministic dependency resolution
  5. Minimal Overhead: Fast resolution and compilation

vais.toml Format

[package]
name = "my-project"
version = "0.1.0"
authors = ["Author Name <author@example.com>"]
description = "A Vais project"
license = "MIT"

[dependencies]
# Local path dependency
my-lib = { path = "../my-lib" }

# Future: Registry dependency (not in initial implementation)
# json-parser = "1.0.0"
# http-client = { version = "2.1.0", features = ["tls"] }

[dev-dependencies]
# Test-only dependencies
test-utils = { path = "../test-utils" }

[build]
# Compiler options
opt-level = 2
debug = false
target = "native"

Directory Structure

my-project/
├── vais.toml           # Package manifest
├── vais.lock           # Locked dependency versions (generated)
├── src/
│   ├── main.vais       # Binary entry point (optional)
│   └── lib.vais        # Library entry point (optional)
├── tests/
│   └── test_*.vais     # Test files
└── .vais-cache/        # Build artifacts (gitignored)
    ├── deps/           # Compiled dependencies
    └── build/          # Intermediate build files

Commands

vais pkg init

Initialize a new package in the current directory.

vais pkg init
# Creates vais.toml with default values

vais pkg build

Build the package and its dependencies.

vais pkg build              # Build with default options
vais pkg build --release    # Build with optimizations
vais pkg build --debug      # Build with debug info

vais pkg check

Type-check the package without generating binaries.

vais pkg check

vais pkg test

Run package tests.

vais pkg test               # Run all tests
vais pkg test test_name     # Run specific test

vais pkg clean

Remove build artifacts.

vais pkg clean

vais pkg add

Add a dependency.

vais pkg add my-lib --path ../my-lib
# Future: vais pkg add json-parser@1.0.0

vais pkg remove

Remove a dependency.

vais pkg remove my-lib

Dependency Resolution

Phase 1 (Initial Implementation)

  1. Parse vais.toml to get dependencies
  2. For each path dependency:
    • Resolve the path relative to the current package
    • Recursively process its vais.toml
    • Build dependency graph
  3. Topological sort dependencies
  4. Compile in order (dependencies first)

Resolution Algorithm

function resolve(package):
    deps = []
    for each dependency in package.dependencies:
        if dependency.path exists:
            dep_package = load_toml(dependency.path / "vais.toml")
            dep_deps = resolve(dep_package)
            deps.extend(dep_deps)
            deps.append(dep_package)
    return deduplicate(deps)

Lock File Format

# vais.lock - DO NOT EDIT
# This file is automatically generated by the package manager

[[package]]
name = "my-lib"
path = "../my-lib"
checksum = "sha256:abc123..."

[[package]]
name = "other-lib"
path = "../other-lib"
checksum = "sha256:def456..."

Build Process

  1. Dependency Resolution

    • Parse vais.toml
    • Resolve all dependencies (transitive)
    • Check for cycles
    • Generate topological order
  2. Incremental Check

    • Compare source checksums with .vais-cache
    • Skip unchanged packages
  3. Compilation

    • For each package in order:
      • Set include paths for dependencies
      • Compile to .ll files
      • Link with dependency objects
  4. Linking

    • Combine all objects
    • Generate final binary

Module Resolution

When compiling with the package manager:

U my-lib/parser    # Resolves to <my-lib-path>/src/parser.vais
U my-lib           # Resolves to <my-lib-path>/src/lib.vais

Resolution order:

  1. Check dependencies in vais.toml
  2. If found, use the dependency's path
  3. Otherwise, fall back to relative path resolution

Implementation Plan

Phase 1: Basic Package Management

  • Design document (this file)
  • vais.toml parsing (toml crate)
  • vais pkg init command
  • Path dependency resolution
  • vais pkg build for single package

Phase 2: Dependency Graph

  • Recursive dependency resolution
  • Cycle detection
  • Topological sorting
  • Lock file generation

Phase 3: Incremental Build

  • Checksum-based change detection
  • .vais-cache management
  • Parallel compilation

Phase 4: Developer Experience

  • vais pkg add/remove commands
  • vais pkg test command
  • Better error messages
  • Progress reporting

Phase 5: Registry

  • Package registry design
  • Version resolution (SemVer)
  • Publishing workflow
  • Workspace support
  • Lockfile deterministic serialization

Error Handling

Common Errors

  1. Missing vais.toml

    error: could not find `vais.toml` in current directory or parent directories
    
  2. Invalid toml

    error: failed to parse vais.toml
      --> vais.toml:5:1
        |
      5 | name =
        | ^ expected value
    
  3. Missing dependency

    error: dependency `my-lib` not found
      --> vais.toml:8:1
        |
      8 | my-lib = { path = "../my-lib" }
        | path `../my-lib` does not exist
    
  4. Dependency cycle

    error: cyclic dependency detected
      package-a -> package-b -> package-a
    

Example Usage

Creating a New Project

mkdir my-project && cd my-project
vais pkg init

# Edit vais.toml to add dependencies
# Create src/main.vais

vais pkg build
./target/my-project

Using a Local Library

# In library directory
cd my-lib
vais pkg init
# Create src/lib.vais with exports

# In application directory
cd my-app
vais pkg add my-lib --path ../my-lib
# Import in src/main.vais: U my-lib/utils
vais pkg build

Semantic Versioning

Vais uses semantic versioning (SemVer) for package version resolution:

Version Specifiers

  • Caret (^): ^1.2.3 — compatible updates (>=1.2.3, <2.0.0)
  • Tilde (~): ~1.2.3 — patch updates only (>=1.2.3, <1.3.0)
  • Exact: =1.2.3 — exact version required
  • Wildcard: 1.* — any patch/minor within major version

Conflict Resolution

When multiple packages depend on different versions:

  1. Attempt to find a compatible version that satisfies all constraints
  2. If no compatible version exists, report a dependency conflict error
  3. Use the highest compatible version that satisfies all constraints

Diamond Dependencies

     A
    / \
   B   C
    \ /
     D

If B depends on D ^1.0 and C depends on D ^1.2, resolve to highest compatible (e.g., D 1.5.0).

Pre-release Versions

Pre-release versions (e.g., 1.0.0-alpha.1) are only used if explicitly specified:

vais-json = "1.0.0-beta.3"    # Must specify pre-release explicitly

Workspace Support

Workspaces allow managing multiple packages in a monorepo:

Workspace Configuration

# workspace-root/vais.toml
[workspace]
members = [
    "crates/vais-core",
    "crates/vais-utils",
    "crates/vais-cli"
]

[workspace.dependencies]
# Shared dependencies for all workspace members
serde = "1.0"

Member Packages

# crates/vais-core/vais.toml
[package]
name = "vais-core"
version = "0.1.0"

[dependencies]
vais-utils = { workspace = true }    # Use workspace member
serde = { workspace = true }          # Use workspace dependency

Workspace Commands

vais pkg build --workspace    # Build all workspace members
vais pkg test --workspace     # Test all workspace members

Lockfile Format

The lockfile (vais.lock) ensures deterministic builds:

# vais.lock - DO NOT EDIT
# This file is automatically generated by the package manager

version = 1

[[package]]
name = "vais-json"
version = "1.2.3"
source = "registry+https://registry.vais.dev/"
checksum = "sha256:abc123def456..."
dependencies = [
    "vais-core 0.1.0",
]

[[package]]
name = "vais-core"
version = "0.1.0"
source = "path+../vais-core"
checksum = "sha256:789xyz012..."
dependencies = []

Lockfile Properties

  • Version — lockfile format version
  • Deterministic — packages sorted alphabetically for stable diffs
  • Checksums — SHA-256 hash of package contents
  • Source — registry URL or local path
  • Transitive — includes all transitive dependencies

Reproducible Builds

The lockfile guarantees:

  1. Same dependency versions across all developers
  2. Same build output given same source
  3. Protection against upstream changes

Registry E2E Testing

The package manager includes comprehensive end-to-end tests:

Test Categories (130 E2E tests)

  1. Init (5 tests) — package creation, default names, duplicate failures, roundtrip, build integration
  2. Install (6 tests) — path dependencies, transitive deps, binary installation, library failures
  3. Publish (3 tests) — request structure, archive creation, server unavailable handling
  4. SemVer (7 tests) — caret/tilde ranges, conflicts, diamond dependencies, pre-release handling
  5. Workspace (3 tests) — member management, inter-member dependencies, manifest parsing
  6. Lockfile (3 tests) — format validation, deterministic serialization, reproducible builds
  7. Templates (4 tests) — binary/library templates, gitignore generation, build verification
  8. Documentation (3 tests) — markdown generation, HTML output, error handling

Running E2E Tests

cargo test --package vaisc registry_e2e_tests

Future Considerations

  1. Features: Conditional compilation with feature flags
  2. Build Scripts: Custom build steps (build.vais)
  3. Binary Distribution: Pre-compiled packages for faster installation
  4. Mirrors: Alternative registry mirrors for reliability
  5. Private Registries: Enterprise-internal package hosting