Software Design Document
Introduction
Purpose
This Software Design Document (SDD) describes the architectural and detailed design of AirGap Deploy, a command-line tool for packaging applications for air-gapped deployment.
This document is intended for:
Developers implementing AirGap Deploy
Code reviewers
Future maintainers
Contributors developing custom components
Scope
This document covers:
System architecture and component decomposition
Data structures and algorithms
Interface specifications
Design decisions and rationales
This document does NOT cover:
Requirements (see SRS)
Implementation details (see source code)
User documentation (see README)
System Architecture
Architectural Style
AirGap Deploy follows a pipeline architecture with distinct stages:
Input (Manifest) → Parse → Collect → Package → Generate Scripts → Output (Archive)
This architecture provides:
Clear separation of concerns: Each stage has a single responsibility
Testability: Each stage can be tested independently
Extensibility: New component types can be added without modifying core pipeline
High-Level Architecture
┌──────────────────────────────────────────────────────────────────┐
│ airgap-deploy CLI │
│ (main.rs) │
└────────────────────────────┬─────────────────────────────────────┘
│
┌────────────┴────────────┐
│ CLI Parser │
│ (clap) │
└────────────┬────────────┘
│
┌────────────────┴────────────────┐
│ │
┌───────▼────────┐ ┌────────▼────────┐
│ prep command │ │ validate, init │
│ │ │ list-components │
└───────┬────────┘ └─────────────────┘
│
├─────────────────────────────────────────┐
│ │
┌───────▼────────┐ ┌────────▼────────┐
│ Manifest Parser│ │ Collector │
│ (manifest.rs) │ │ (collector.rs) │
└───────┬────────┘ └────────┬────────┘
│ │
│ produces │ uses
│ │
┌───────▼────────┐ ┌────────▼────────┐
│ Manifest │ │ Component │
│ (struct) │ │ Registry │
└────────────────┘ │ (registry.rs) │
└────────┬────────┘
│
┌────────────┴─────────────┐
│ │
┌────────▼────────┐ ┌─────────▼────────┐
│ RustApp │ │ ExternalBinary │
│ Component │ │ Component │
└─────────────────┘ └──────────────────┘
┌─────────────────┐ ┌──────────────────┐
│ ModelFile │ │ SystemPackage │
│ Component │ │ Component │
└─────────────────┘ └──────────────────┘
│
│ collected files
│
┌────────▼────────┐
│ Packager │
│ (packager.rs) │
└────────┬────────┘
│
├─────────────────┐
│ │
┌────────▼────────┐ ┌─────▼───────────┐
│ Install Script │ │ Package Archive │
│ Generator │ │ (.tar.gz/.zip) │
│ (installer.rs) │ │ │
└─────────────────┘ └─────────────────┘
Component Descriptions
Component |
Responsibility |
Key Types |
|---|---|---|
CLI Parser |
Parse command-line arguments |
|
Manifest Parser |
Parse and validate TOML manifests |
|
Component Registry |
Register and instantiate component types |
|
Collector |
Orchestrate component collection |
|
Built-in Components |
Implement specific component types |
|
Packager |
Create deployment archives |
|
Install Script Generator |
Generate installation scripts |
|
Detailed Design
Core Data Structures
Manifest Structure
/// Top-level manifest structure
pub struct Manifest {
pub package: PackageConfig,
pub targets: Option<TargetsConfig>,
pub components: Vec<ComponentConfig>,
pub install: Option<InstallConfig>,
}
/// Package metadata
pub struct PackageConfig {
pub name: String,
pub version: String,
pub description: String,
}
/// Target platforms configuration
pub struct TargetsConfig {
pub platforms: Vec<Platform>,
pub default: Platform,
}
/// Platform identification
pub enum Platform {
LinuxX86_64,
LinuxAarch64,
MacOSX86_64,
MacOSAarch64,
WindowsX86_64,
}
/// Component configuration (polymorphic)
pub struct ComponentConfig {
pub component_type: String, // "rust-app", "external-binary", etc.
pub config: serde_json::Value, // Component-specific config
}
/// Installation configuration
pub struct InstallConfig {
pub method: InstallMethod,
pub install_to: InstallLocation,
pub mode: InstallMode,
pub config: Option<ConfigTemplate>,
pub steps: Option<HashMap<String, Vec<String>>>,
pub prefix: Option<PathBuf>, // Installation root directory
pub user: Option<String>, // System user for service
pub group: Option<String>, // System group for service
}
pub enum InstallMethod {
BuildFromSource,
PrebuiltBinary,
}
pub enum InstallLocation {
User, // ~/.local or %LOCALAPPDATA%
System, // /usr/local or C:\Program Files
}
pub enum InstallMode {
Interactive,
Automatic,
}
Component Trait
/// Trait that all components must implement
pub trait Component: Send + Sync {
/// Collect this component's files to the staging directory
fn collect(&self, staging_dir: &Path, context: &CollectionContext) -> Result<CollectionResult>;
/// Get the component's name
fn name(&self) -> &str;
/// Get installation steps for this component
fn install_steps(&self) -> Vec<InstallStep>;
/// Validate component configuration
fn validate(&self) -> Result<()>;
}
/// Context provided during collection
pub struct CollectionContext {
pub target_platform: Platform,
pub cache_dir: PathBuf,
pub progress: ProgressBar,
}
/// Result of component collection
pub struct CollectionResult {
pub collected_files: Vec<CollectedFile>,
pub metadata: ComponentMetadata,
}
pub struct CollectedFile {
pub source_path: PathBuf,
pub relative_path: PathBuf, // Path within package
pub checksum: Option<String>,
}
Install Steps
/// Installation step for generated scripts
pub enum InstallStep {
CheckDependency {
name: String,
command: String, // e.g., "rustc --version"
install_if_missing: bool,
},
ExecuteCommand {
command: String,
working_dir: Option<PathBuf>,
description: String,
},
CopyFile {
source: PathBuf,
destination: PathBuf,
permissions: Option<u32>,
},
GenerateConfig {
template: String,
output_path: PathBuf,
variables: HashMap<String, String>,
},
CreateDirectory {
path: PathBuf,
permissions: Option<u32>,
},
}
Component Implementations
RustAppComponent
Purpose: Package Rust applications with vendored dependencies
Configuration:
[[components]]
type = "rust-app"
source = "."
vendor = true
include_toolchain = true
prebuild = false
Collection Algorithm:
Locate Cargo.toml in source directory
If vendor = true:
Execute
cargo vendorto download dependenciesCreate
.cargo/config.tomlwith vendored paths
If include_toolchain = true:
Download Rust toolchain installer from static.rust-lang.org
Include in
rust-installer/directory
Copy source code and vendor directory to staging
If prebuild = true:
Execute
cargo build --releaseInclude prebuilt binary (deferred to v0.2)
Install Steps:
vec![
InstallStep::CheckDependency {
name: "Rust".to_string(),
command: "rustc --version".to_string(),
install_if_missing: true, // Use included installer
},
InstallStep::ExecuteCommand {
command: "cargo build --release --offline".to_string(),
working_dir: Some(PathBuf::from("app-source")),
description: "Building Rust application".to_string(),
},
InstallStep::CopyFile {
source: PathBuf::from("app-source/target/release/app-name"),
destination: PathBuf::from("{{ install_prefix }}/bin/app-name"),
permissions: Some(0o755),
},
]
ExternalBinaryComponent
Purpose: Include external binaries that need to be built from source
Configuration:
[[components]]
type = "external-binary"
name = "whisper.cpp"
repo = "https://github.com/ggerganov/whisper.cpp.git"
tag = "v1.0.0"
build_instructions = "make"
Collection Algorithm: 1. Clone Git repository to staging directory 2. Checkout specified branch/tag/commit 3. Include entire repository (for offline build) 4. Store build instructions for install script
Install Steps:
vec![
InstallStep::CheckDependency {
name: "C compiler".to_string(),
command: "gcc --version || clang --version".to_string(),
install_if_missing: false,
},
InstallStep::ExecuteCommand {
command: "make".to_string(), // From build_instructions
working_dir: Some(PathBuf::from("whisper.cpp")),
description: "Building whisper.cpp".to_string(),
},
InstallStep::CopyFile {
source: PathBuf::from("whisper.cpp/main"),
destination: PathBuf::from("{{ install_prefix }}/bin/whisper-main"),
permissions: Some(0o755),
},
]
ModelFileComponent
Purpose: Download large model files with checksum verification
Configuration:
[[components]]
type = "model-file"
name = "base.en"
url = "https://huggingface.co/ggerganov/whisper.cpp/resolve/main/ggml-base.en.bin"
checksum = "sha256:abc123..."
required = true
install_path = "models/base.en.bin"
Collection Algorithm:
Check cache directory for existing file with matching checksum If not cached:
Download file from URL with progress bar
Support resume via HTTP Range requests
Stream to disk (don’t load entire file in memory)
Verify SHA-256 checksum Copy to staging directory
Install Steps:
vec![
InstallStep::CreateDirectory {
path: PathBuf::from("{{ install_prefix }}/share/app-name/models"),
permissions: Some(0o755),
},
InstallStep::CopyFile {
source: PathBuf::from("models/base.en.bin"),
destination: PathBuf::from("{{ install_prefix }}/share/app-name/models/base.en.bin"),
permissions: Some(0o644),
},
]
ConfigFileComponent
Purpose: Deploy configuration files (e.g., systemd units, config templates) with inline content
Configuration:
[[components]]
type = "config-file"
name = "ollama.service"
content = """
[Unit]
Description=Ollama LLM Server
After=network.target
[Service]
Type=simple
User=ollama
ExecStart=/opt/ollama/bin/ollama serve
[Install]
WantedBy=multi-user.target
"""
install_path = "/etc/systemd/system/ollama.service"
Collection Algorithm:
Write inline
contentto a file in the staging directoryRecord
install_pathfor the install script
Install Steps:
vec![
InstallStep::CopyFile {
source: PathBuf::from("config-files/ollama.service"),
destination: PathBuf::from("/etc/systemd/system/ollama.service"),
permissions: Some(0o644),
},
]
GithubReleaseSource (ExternalBinaryComponent Variant)
Purpose: Download prebuilt binaries from GitHub Releases (no build step needed)
Configuration:
[[components]]
type = "external-binary"
name = "ollama"
source.type = "github-release"
source.repo = "ollama/ollama"
source.tag = "v0.15.0"
source.asset_pattern = "ollama-linux-amd64"
install_path = "bin/ollama"
Collection Algorithm:
Resolve GitHub Releases API URL:
https://github.com/{repo}/releases/download/{tag}/{asset_pattern}Download matching release asset with progress bar
Verify checksum if provided
Copy binary to staging directory
Note: Unlike the standard ExternalBinaryComponent which clones a Git repository and builds from source, GithubReleaseSource downloads a prebuilt binary directly. No build_instructions field is needed.
Packaging Algorithm
Input: Staging directory with collected components Output: Compressed archive (.tar.gz or .zip)
Algorithm:
1. Create package layout in staging directory:
- install.sh / install.ps1 (generated)
- README.txt (generated)
- airgap-deploy-metadata.json (generated)
- Component files (already collected)
2. Generate install scripts:
a. Render Tera templates with component install steps
b. Include dependency checking logic
c. Include error handling and logging
d. Include interactive/automatic mode support
3. Generate README.txt:
- Package name, version, description
- Installation instructions
- Component list
- System requirements
4. Generate metadata JSON:
{
"package_name": "...",
"version": "...",
"target_platform": "...",
"components": [...],
"created_at": "...",
"airgap_deploy_version": "..."
}
5. Create archive:
- Linux/macOS: tar czf package.tar.gz package-dir/
- Windows: Zip package.zip package-dir/
6. Generate package checksum:
- SHA-256 of final archive
- Save to package.tar.gz.sha256
Install Script Template
Bash Template (install.sh.tera):
#!/bin/bash
set -e
# Generated by airgap-deploy {{ airgap_deploy_version }}
# Package: {{ package_name }} v{{ package_version }}
# Target: {{ target_platform }}
INSTALL_PREFIX="${INSTALL_PREFIX:-$HOME/.local}"
MODE="${MODE:-interactive}"
LOG_FILE="install.log"
echo "=== {{ package_name }} Installation ===" | tee -a "$LOG_FILE"
echo "Target: {{ target_platform }}" | tee -a "$LOG_FILE"
echo "" | tee -a "$LOG_FILE"
# Dependency checking
{% for dep in dependencies %}
if ! command -v {{ dep.command }} &> /dev/null; then
{% if dep.install_if_missing %}
echo "Installing {{ dep.name }}..." | tee -a "$LOG_FILE"
# Install logic here
{% else %}
echo "ERROR: {{ dep.name }} not found. Please install it first." | tee -a "$LOG_FILE"
exit 1
{% endif %}
fi
{% endfor %}
# Interactive mode: prompt for install location
if [ "$MODE" = "interactive" ]; then
read -p "Installation directory [$INSTALL_PREFIX]: " USER_PREFIX
INSTALL_PREFIX="${USER_PREFIX:-$INSTALL_PREFIX}"
fi
echo "Installing to: $INSTALL_PREFIX" | tee -a "$LOG_FILE"
# Execute install steps
{% for step in install_steps %}
echo "{{ step.description }}" | tee -a "$LOG_FILE"
{{ step.command }} 2>&1 | tee -a "$LOG_FILE"
{% endfor %}
echo "" | tee -a "$LOG_FILE"
echo "Installation complete!" | tee -a "$LOG_FILE"
echo "Installed to: $INSTALL_PREFIX" | tee -a "$LOG_FILE"
Interface Specifications
Component Plugin Interface
Future Enhancement: Plugin system for custom component types
// Plugin trait (future)
pub trait ComponentPlugin {
fn name(&self) -> &str;
fn version(&self) -> &str;
fn create_component(&self, config: serde_json::Value) -> Result<Box<dyn Component>>;
}
// Plugin discovery (future)
pub struct PluginLoader {
plugin_dir: PathBuf,
}
impl PluginLoader {
pub fn load_plugins(&self) -> Result<Vec<Box<dyn ComponentPlugin>>> {
// Load dynamic libraries from plugin_dir
// Instantiate plugins
// Validate API version compatibility
}
}
Note: Plugin system deferred to v1.1+ (see Roadmap Phase 7)
Design Decisions and Rationales
Why TOML for Manifests?
Decision: Use TOML format for AirGapDeploy.toml
Alternatives Considered:
YAML: Too flexible, whitespace-sensitive
JSON: Not human-friendly for configuration
Custom DSL: Too much complexity
Rationale:
TOML is human-readable and writable
Strongly typed (better validation)
Good Rust ecosystem support (serde, toml crate)
Used by Cargo.toml (familiar to Rust developers)
Why Build from Source on Air-Gapped System?
Decision: Build applications on air-gapped system rather than prebuilding
Alternatives Considered:
Prebuild binaries for all platforms
Cross-compilation on developer machine
Rationale:
Trust: User can inspect source before building
Flexibility: Supports different system configurations (ALSA, GPU, etc.)
Simplicity: No cross-compilation toolchain setup required
MVP Scope: Prebuilding can be added later as optional optimization
Trade-off: Requires build tools on air-gapped system, longer installation time
Why Component Trait Instead of Enums?
Decision: Use trait-based design for components
Alternatives Considered:
Enum with variants for each component type
Struct with function pointers
Rationale:
Extensibility: Easy to add new component types without modifying core
Plugin System: Enables future plugin architecture
Separation of Concerns: Each component type is independent module
Testing: Can mock components for unit tests
Trade-off: Slightly more complex than enum, uses dynamic dispatch
Why Generate Install Scripts Instead of Installing Directly?
Decision: Generate standalone installation scripts
Alternatives Considered:
airgap-deploy installcommand that must run on air-gapped systemRequire AirGap Deploy binary on both sides
Rationale:
Self-contained: Package includes everything needed, no external dependencies
Inspectable: User can review install script before running
Platform-native: Bash/PowerShell are standard on target systems
Flexibility: User can customize script if needed
Trade-off: Less control over installation process, script generation complexity
Error Handling Strategy
Error Types
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error("Manifest parse error: {0}")]
ManifestParse(#[from] toml::de::Error),
#[error("Component collection failed: {0}")]
ComponentCollection(String),
#[error("Network error: {0}")]
Network(#[from] reqwest::Error),
#[error("IO error: {0}")]
Io(#[from] std::io::Error),
#[error("Checksum mismatch for {file}: expected {expected}, got {actual}")]
ChecksumMismatch {
file: String,
expected: String,
actual: String,
},
#[error("Component not found: {0}")]
ComponentNotFound(String),
}
Error Recovery
Network Errors:
Retry up to 3 times with exponential backoff
Display clear message with retry attempt number
Suggest checking network connection
Checksum Errors:
Display expected vs actual checksum
Suggest re-downloading file or updating manifest
Do NOT proceed with mismatched checksums
IO Errors:
Display file path and operation that failed
Suggest checking disk space and permissions
Clean up temporary files on failure
Logging
Levels:
ERROR: Critical failures that prevent completion
WARN: Non-fatal issues that might affect operation
INFO: Normal operational messages (default)
DEBUG: Detailed information for troubleshooting (–verbose)
TRACE: Very detailed, internal state information
Output:
Console: INFO and above (with colors)
Log file (optional): DEBUG and above
Performance Considerations
Parallel Collection
Optimization: Collect components in parallel using rayon
use rayon::prelude::*;
pub fn collect_all_components(
components: Vec<Box<dyn Component>>,
staging_dir: &Path,
context: &CollectionContext,
) -> Result<Vec<CollectionResult>> {
components
.par_iter() // Parallel iterator
.map(|component| component.collect(staging_dir, context))
.collect::<Result<Vec<_>>>()
}
Benefit: Reduces total collection time by 50-70% for manifests with multiple independent components
Streaming Downloads
Optimization: Stream large files to disk, don’t load in memory
pub fn download_file(url: &str, dest: &Path) -> Result<()> {
let mut response = reqwest::blocking::get(url)?;
let mut file = File::create(dest)?;
// Stream in chunks
std::io::copy(&mut response, &mut file)?;
Ok(())
}
Benefit: Constant memory usage regardless of file size (can handle 50GB+ files)
Caching
Optimization: Cache downloaded files by checksum
pub fn get_cached_file(url: &str, checksum: &str, cache_dir: &Path) -> Option<PathBuf> {
let cache_path = cache_dir.join(checksum);
if cache_path.exists() && verify_checksum(&cache_path, checksum).is_ok() {
Some(cache_path)
} else {
None
}
}
Benefit: Skip re-downloading large model files if already present (saves bandwidth and time)
Security Considerations
Checksum Verification
All downloaded files MUST be verified with SHA-256 checksums:
pub fn verify_checksum(file_path: &Path, expected: &str) -> Result<()> {
let mut file = File::open(file_path)?;
let mut hasher = Sha256::new();
std::io::copy(&mut file, &mut hasher)?;
let actual = format!("{:x}", hasher.finalize());
if actual != expected {
return Err(Error::ChecksumMismatch {
file: file_path.display().to_string(),
expected: expected.to_string(),
actual,
});
}
Ok(())
}
Temporary File Security
Temporary files MUST have restrictive permissions:
use std::os::unix::fs::PermissionsExt;
pub fn create_temp_file() -> Result<File> {
let file = tempfile::NamedTempFile::new()?;
#[cfg(unix)]
{
let mut perms = file.as_file().metadata()?.permissions();
perms.set_mode(0o600); // User read/write only
file.as_file().set_permissions(perms)?;
}
Ok(file)
}
No Arbitrary Code Execution
Manifests MUST NOT allow arbitrary code execution:
NO
eval()or similar dynamic executionBuild commands are templated strings, not code
Install steps are data structures, not scripts
Testing Strategy
Unit Tests
Coverage: Each module (manifest, components, packager, installer)
Example:
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_manifest_parse_valid() {
let toml = r#"
[package]
name = "test"
version = "1.0.0"
description = "Test package"
"#;
let manifest: Manifest = toml::from_str(toml).unwrap();
assert_eq!(manifest.package.name, "test");
}
#[test]
fn test_checksum_verification() {
// Create test file with known content
// Verify correct checksum passes
// Verify incorrect checksum fails
}
}
Integration Tests
Scenario: End-to-end package creation and installation
#[test]
fn test_whisper_lite_packaging() {
// Create minimal AirGapDeploy.toml for test app
// Run `airgap-deploy prep`
// Verify package structure
// Extract package in temp directory
// Run install script in test environment
// Verify installation succeeded
}
Platform Testing
CI Matrix:
Linux: Ubuntu 20.04, Ubuntu 22.04, Fedora 38
macOS: macOS 12 (Intel), macOS 14 (Apple Silicon)
Windows: Windows 10, Windows 11
Future Enhancements
Pre-built Binaries (v0.2)
Add prebuild = true option to build binaries on developer machine:
[[components]]
type = "rust-app"
source = "."
prebuild = true
target = "x86_64-unknown-linux-gnu"
Requires: Cross-compilation toolchain setup
Digital Signatures (v1.1)
Sign packages for verification on air-gapped systems:
airgap-deploy prep --sign-with ~/.gnupg/key.asc
Requires: GPG integration, key management
Plugin System (v2.0)
See Roadmap (Phase 7)