robust-dependency-installation
npx skills add https://github.com/securityronin/ronin-marketplace --skill robust-dependency-installation
Agent 安装分布
Skill 文档
Robust Dependency Installation
See also: For the full DMG/MSI/DEB packaging workflow, GitHub Actions release automation, and Homebrew updates, see the
build-cross-platform-packagesskill.
When to Use This Skill
Use this skill when:
- Building Windows MSI installers that need to include external dependencies
- Implementing unified dependency detection and execution systems
- Working with portable executable distributions
- Users report dependencies not being found despite successful installation
Core Principles
1. Bundled Dependencies Over External Package Managers
Modern approach: Bundle portable executables directly in the installer.
Traditional Approach (DEPRECATED):
Layer 1: Scoop/Chocolatey with DNS fallback
Layer 2: Alternative package manager
Layer 3: Public DNS retry
Layer 4: Manual instructions
Modern Approach (RECOMMENDED):
Layer 1: Bundled portable executables in MSI
- No network required during installation
- Specific tested versions
- ~100% installation success rate
- No dependency on external package managers
Why bundled dependencies:
- â Works offline (no network required)
- â Predictable versions (regression testing possible)
- â Single infrastructure (your releases)
- â No DNS/firewall/VPN issues
- â Instant installation (no downloads)
- â ~100% success rate
Trade-offs:
- â Larger installer size (~50-200 MB)
- â Must update bundled versions manually
- â Legal compliance overhead (license tracking)
2. Bundled Dependency Architecture (Proven in Production)
Complete workflow from download to MSI:
# GitHub Actions workflow
jobs:
prepare-bundled-deps:
runs-on: ubuntu-latest
steps:
- name: Download portable dependencies
run: |
# ExifTool (portable Perl)
curl -L https://exiftool.org/exiftool-13.41_64.zip -o exiftool.zip
# Tesseract (Windows installer, extract with 7z)
curl -L https://github.com/UB-Mannheim/tesseract/.../tesseract.exe -o tesseract.exe
# FFmpeg (static build)
curl -L https://github.com/BtbN/FFmpeg-Builds/.../ffmpeg.zip -o ffmpeg.zip
# ImageMagick (portable)
curl -L https://github.com/ImageMagick/.../ImageMagick.7z -o imagemagick.7z
- name: Extract and organize
run: |
mkdir -p deps/{exiftool,tesseract,ffmpeg,imagemagick}
unzip exiftool.zip -d deps/exiftool/
7z x tesseract.exe -o deps/tesseract/
unzip ffmpeg.zip && cp bin/* deps/ffmpeg/
7z x imagemagick.7z -o deps/imagemagick/
- name: Create single ZIP
run: |
cd deps
zip -r ../deps-windows.zip .
- name: Upload as artifact (not release asset)
uses: actions/upload-artifact@v4
with:
name: bundled-deps-windows
path: deps-windows.zip
retention-days: 7
build-msi:
needs: prepare-bundled-deps
runs-on: windows-latest
steps:
- name: Download bundled deps artifact
uses: actions/download-artifact@v4
with:
name: bundled-deps-windows
path: .
- name: Extract for WiX
run: |
Expand-Archive deps-windows.zip -Destination target/bundled-deps
- name: Build MSI
run: wix build installer/app.wxs
Key points:
- Deps uploaded as GitHub Actions artifact (7-day retention)
- NOT uploaded to release page (reduces user confusion)
- MSI downloads from workflow artifacts, not release assets
- All deps embedded in final MSI
3. WiX Installer Structure
Directory structure in MSI:
<Package Name="myapp" Version="1.0.0">
<StandardDirectory Id="ProgramFiles64Folder">
<Directory Id="INSTALLFOLDER" Name="myapp">
<!-- Application binaries -->
<Directory Id="DEPSFOLDER" Name="deps">
<Directory Id="EXIFTOOLFOLDER" Name="exiftool" />
<Directory Id="TESSERACTFOLDER" Name="tesseract" />
<Directory Id="FFMPEGFOLDER" Name="ffmpeg" />
<Directory Id="IMAGEMAGICKFOLDER" Name="imagemagick" />
</Directory>
</Directory>
</StandardDirectory>
<!-- ExifTool Component -->
<DirectoryRef Id="EXIFTOOLFOLDER">
<Component Id="ExifToolDep">
<File Source="target\bundled-deps\exiftool\exiftool.exe" KeyPath="yes" />
<Environment Id="PATH_EXIFTOOL"
Name="PATH"
Value="[EXIFTOOLFOLDER]"
Permanent="yes"
Part="last"
Action="set"
System="yes" />
</Component>
</DirectoryRef>
<!-- Similar for Tesseract, FFmpeg, ImageMagick -->
</Package>
Installation result:
C:\Program Files\myapp\
âââ myapp.exe
âââ myapp-gui.exe
âââ deps\
âââ exiftool\exiftool.exe
âââ tesseract\tesseract.exe
âââ ffmpeg\ffmpeg.exe + ffprobe.exe
âââ imagemagick\magick.exe
Each directory automatically added to system PATH during installation.
4. Unified Dependency Detection and Execution
THE CRITICAL RULE: Detection and execution MUST use identical path resolution.
Problem: Split code paths cause “detected but won’t execute” bugs.
// â WRONG - Duplicate logic
fn is_tool_available() -> bool {
Command::new("tool").output().is_ok() // PATH only
}
fn use_tool() {
Command::new("tool")... // Different code path!
}
Solution: Single source of truth for path resolution.
#[derive(Debug, Clone, PartialEq)]
pub enum Dependency {
ExifTool,
Tesseract,
FFmpeg,
ImageMagick,
}
impl Dependency {
pub fn name(&self) -> &str {
match self {
Dependency::ExifTool => "exiftool",
Dependency::Tesseract => "tesseract",
Dependency::FFmpeg => "ffmpeg",
Dependency::ImageMagick => "imagemagick",
}
}
/// Directory name where MSI installs the tool
fn bundled_dir_name(&self) -> &str {
match self {
Dependency::ExifTool => "exiftool",
Dependency::Tesseract => "tesseract",
Dependency::FFmpeg => "ffmpeg",
Dependency::ImageMagick => "imagemagick",
}
}
/// Actual executable name (may differ from directory name)
fn exe_name(&self) -> &str {
match self {
Dependency::ExifTool => "exiftool",
Dependency::Tesseract => "tesseract",
Dependency::FFmpeg => "ffmpeg",
Dependency::ImageMagick => "magick", // ImageMagick v7+ uses magick.exe
}
}
/// Find executable path - checks bundled location FIRST
pub fn find_executable(&self) -> Option<PathBuf> {
#[cfg(windows)]
{
// Priority 1: Bundled MSI location
if let Ok(programfiles) = std::env::var("PROGRAMFILES") {
let bundled_dir = PathBuf::from(&programfiles)
.join("myapp")
.join("deps")
.join(self.bundled_dir_name());
let exe_path = bundled_dir.join(format!("{}.exe", self.exe_name()));
if exe_path.exists() {
return Some(exe_path);
}
}
}
// Priority 2: System PATH
if which::which(self.exe_name()).is_ok() {
return Some(PathBuf::from(self.exe_name()));
}
// Priority 3: Fallback names (e.g., "convert" for ImageMagick)
for name in self.fallback_names() {
if which::which(name).is_ok() {
return Some(PathBuf::from(name));
}
}
None
}
/// Create Command - ALWAYS uses find_executable()
pub fn create_command(&self) -> Option<Command> {
self.find_executable().map(Command::new)
}
/// Check availability - uses create_command()
pub fn is_available(&self) -> bool {
if let Some(mut cmd) = self.create_command() {
let result = match self {
Dependency::ExifTool => cmd.arg("-ver").output(),
Dependency::Tesseract => cmd.arg("--version").output(),
Dependency::FFmpeg => cmd.arg("-version").output(),
Dependency::ImageMagick => cmd.arg("-version").output(),
};
result.map(|o| o.status.success()).unwrap_or(false)
} else {
false
}
}
fn fallback_names(&self) -> &[&str] {
match self {
Dependency::ImageMagick => &["magick", "convert"],
_ => &[],
}
}
}
Usage in application code:
// â
CORRECT - Unified code path
fn run_ocr(image_path: &Path) -> Result<String> {
let mut cmd = Dependency::Tesseract
.create_command()
.context("Tesseract not available")?;
let output = cmd
.arg(image_path)
.arg("stdout")
.output()?;
Ok(String::from_utf8(output.stdout)?)
}
Critical details:
- Bundled location checked FIRST (highest priority)
- Detection (
is_available()) uses same code as execution (create_command()) - Handles case where directory name â executable name (imagemagick/magick.exe)
- Works even if PATH not yet updated (MSI-spawned processes)
5. GUI PATH Setup for Bundled Dependencies
Problem: GUI launched from MSI inherits old environment before PATH refresh.
Solution: Prepend bundled directories to PATH at application startup.
// In main.rs - call before any dependency usage
fn setup_path() {
use std::env;
let current_path = env::var("PATH").unwrap_or_default();
let mut additional_paths: Vec<String> = if cfg!(windows) {
let mut paths = Vec::new();
// Bundled MSI installer dependencies (HIGHEST PRIORITY)
if let Ok(programfiles) = env::var("PROGRAMFILES") {
paths.push(format!("{}\\myapp\\deps\\exiftool", programfiles));
paths.push(format!("{}\\myapp\\deps\\tesseract", programfiles));
paths.push(format!("{}\\myapp\\deps\\ffmpeg", programfiles));
paths.push(format!("{}\\myapp\\deps\\imagemagick", programfiles));
}
paths
} else {
vec![]
};
// Build new PATH
if cfg!(windows) {
let new_path = if additional_paths.is_empty() {
current_path
} else {
format!("{};{}", additional_paths.join(";"), current_path)
};
env::set_var("PATH", new_path);
}
}
fn main() {
setup_path(); // Call FIRST
// Rest of application logic...
}
Why this works:
- Runs at application startup before any dependency checks
- Doesn’t rely on system PATH being updated
- Works for MSI-spawned processes with stale environment
- Bundled locations have highest priority
Legal Compliance for Bundled Dependencies
When bundling portable executables, you MUST comply with licenses:
License Verification Checklist
-
Verify all licenses allow redistribution:
- GPL: â Allowed with source code availability
- LGPL: â Allowed with source/link
- Apache 2.0: â Allowed with attribution
- MIT: â Allowed with attribution
- Proprietary: â Check terms carefully
-
Create
/LICENSES/directory with license texts -
Create
/third_party/<dep>/NOTICEattribution files -
For LGPL (FFmpeg): Include source link or code
-
Update README.md with third-party attributions
-
Create
.gitignorein/deps/(never commit binaries to git)
REUSE spec structure:
project/
âââ LICENSES/
â âââ GPL-3.0-or-later.txt
â âââ Apache-2.0.txt
â âââ MIT.txt
âââ third_party/
â âââ exiftool/
â â âââ NOTICE
â âââ tesseract/
â â âââ NOTICE
â âââ ffmpeg/
â âââ NOTICE (include source link for LGPL)
âââ README.md (attribution section)
Example NOTICE file:
ExifTool by Phil Harvey
License: GPL-1.0-or-later OR Artistic-2.0
Homepage: https://exiftool.org
Bundled version: 13.41
Copyright (c) 2003-2024 Phil Harvey
Implementation Checklist
Bundled Dependency Setup
- Create GitHub Actions workflow to download portable executables
- Organize deps into directory structure (one folder per tool)
- Create single ZIP artifact (not release asset)
- Add download-artifact step in MSI build job
- Extract to
target/bundled-deps/before WiX build
WiX Installer Configuration
- Define directory structure with DEPSFOLDER
- Create Component for each dependency with File elements
- Add Environment PATH entries for each directory
- Set Permanent=”yes” for PATH changes
- Include all Components in Feature
Application Code
- Create Dependency enum with all external tools
- Implement bundled_dir_name() and exe_name() methods
- Implement find_executable() checking bundled location first
- Add create_command() and is_available() using find_executable()
- Replace ALL Command::new(“tool”) with Dependency::Tool.create_command()
- Add setup_path() function in main.rs (GUI applications)
Legal Compliance
- Verify all dependency licenses allow redistribution
- Create LICENSES/ directory with license texts
- Create third_party/ notices
- Update README with attributions
- Add .gitignore for deps/ directory
Testing
- Test MSI installation on clean Windows VM
- Verify dependencies installed to Program Files\myapp\deps\
- Verify system PATH updated with dependency directories
- Test GUI launched from MSI (before PATH refresh)
- Test CLI from terminal (after PATH refresh)
- Verify all dependencies detected as available
- Test actual execution (not just detection)
Dependency-Specific Notes
ExifTool
- Format: Portable Perl executable (single .exe)
- Source: https://exiftool.org/exiftool-{version}_64.zip
- License: GPL-1.0-or-later OR Artistic-2.0
- Installation: Extract .exe to deps/exiftool/
Tesseract OCR
- Format: Windows installer (.exe), extract with 7z
- Source: https://github.com/UB-Mannheim/tesseract/releases
- License: Apache-2.0
- Installation:
7z x tesseract-setup.exe -o deps/tesseract/ - Note: Include tessdata/ directory for OCR languages
FFmpeg
- Format: Static build ZIP
- Source: https://github.com/BtbN/FFmpeg-Builds/releases
- License: LGPL-2.1 (must include source link or code)
- Installation: Extract bin/ffmpeg.exe and bin/ffprobe.exe
- Version pinning: Use specific release tags (e.g.,
autobuild-2024-11-04-12-55)
ImageMagick
- Format: Portable 7z archive
- Source: https://github.com/ImageMagick/ImageMagick/releases
- License: Apache-2.0
- Installation: Extract all .exe and .dll files
- Note: v7+ uses magick.exe (not imagemagick.exe)
Anti-Patterns to Avoid
â Uploading Deps to GitHub Release Page
# WRONG - Visible to end users, causes confusion
- name: Upload to release
uses: softprops/action-gh-release@v2
with:
files: deps-windows.zip
Problem: Users think they need to download both MSI and ZIP.
Solution: Use GitHub Actions artifacts instead.
â Split Detection and Execution Logic
// WRONG - Duplicate logic
fn is_tool_available() -> bool {
Command::new("tool").output().is_ok()
}
fn use_tool() {
Command::new("tool")... // Different path!
}
Problem: Detection succeeds but execution fails.
Solution: Both use Dependency::Tool methods.
â Checking PATH Only
// WRONG - Fails for MSI-spawned processes
pub fn find_executable(&self) -> Option<PathBuf> {
which::which(self.name()).ok()
}
Problem: MSI-spawned GUI has stale PATH.
Solution: Check bundled location FIRST, then PATH.
â Directory Name = Executable Name Assumption
// WRONG - Assumes directory matches executable
let path = bundled_dir.join(format!("{}.exe", self.name()));
Problem: ImageMagick installed to imagemagick/ but executable is magick.exe.
Solution: Separate bundled_dir_name() and exe_name().
Real-World Results
From production deployment (bundled dependency architecture):
â Installation success rate: ~100% (up from ~90% with Scoop/Chocolatey)
- No network failures
- No DNS resolution issues
- No firewall/VPN interference
- Works in air-gapped environments
â Instant dependency availability
- No download time during installation
- Predictable performance
â Version control
- ExifTool 13.41, Tesseract 5.5.0, FFmpeg 7.1, ImageMagick 7.1.2-8
- Regression testing possible
- No “works on my machine” version mismatches
â Detection reliability
- GUI detects dependencies immediately when launched from MSI
- Works before PATH environment refresh
- Unified code path prevents detection/execution split
Common issue resolved: GUI showing “dependencies missing” despite successful MSI installation.
Root cause: MSI-spawned GUI inherited old environment without updated PATH.
Solution: Bundled location checked first, GUI setup_path() prepends at startup.
Summary
Modern dependency installation uses bundled portable executables embedded in MSI installers.
Key principles:
- Bundle portable executables in MSI (no external package managers)
- Check bundled location FIRST in path resolution
- Unified detection and execution code paths
- GUI prepends bundled directories at startup
- Legal compliance with license attribution
- GitHub Actions artifact (not release asset)
Directory structure:
C:\Program Files\myapp\deps\
âââ exiftool\exiftool.exe
âââ tesseract\tesseract.exe (+ tessdata/)
âââ ffmpeg\ffmpeg.exe + ffprobe.exe
âââ imagemagick\magick.exe (+ DLLs)
Detection priority:
- Bundled MSI location (
C:\Program Files\myapp\deps\) - System PATH
- Fallback names (magick vs convert)
Result: ~100% installation success, instant availability, version control, no network dependency.