| name | robust-dependency-installation |
| description | Use when implementing dependency installation systems - covers bundled portable executables in MSI installers, unified detection/execution code paths, and Windows installer best practices |
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-packages skill.
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:
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
curl -L https://github.com/UB-Mannheim/tesseract/.../tesseract.exe -o tesseract.exe
curl -L https://github.com/BtbN/FFmpeg-Builds/.../ffmpeg.zip -o ffmpeg.zip
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">
<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>
<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>
</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.
fn is_tool_available() -> bool {
Command::new("tool").output().is_ok()
}
fn use_tool() {
Command::new("tool")...
}
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",
}
}
fn bundled_dir_name(&self) -> &str {
match self {
Dependency::ExifTool => "exiftool",
Dependency::Tesseract => "tesseract",
Dependency::FFmpeg => "ffmpeg",
Dependency::ImageMagick => "imagemagick",
}
}
fn exe_name(&self) -> &str {
match self {
Dependency::ExifTool => "exiftool",
Dependency::Tesseract => "tesseract",
Dependency::FFmpeg => "ffmpeg",
Dependency::ImageMagick => "magick",
}
}
pub fn find_executable(&self) -> Option<PathBuf> {
#[cfg(windows)]
{
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);
}
}
}
if which::which(self.exe_name()).is_ok() {
return Some(PathBuf::from(self.exe_name()));
}
for name in self.fallback_names() {
if which::which(name).is_ok() {
return Some(PathBuf::from(name));
}
}
None
}
pub fn create_command(&self) -> Option<Command> {
self.find_executable().map(Command::new)
}
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:
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.
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();
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![]
};
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();
}
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
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
WiX Installer Configuration
Application Code
Legal Compliance
Testing
Dependency-Specific Notes
ExifTool
Tesseract OCR
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
Anti-Patterns to Avoid
❌ Uploading Deps to GitHub Release Page
- 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
fn is_tool_available() -> bool {
Command::new("tool").output().is_ok()
}
fn use_tool() {
Command::new("tool")...
}
Problem: Detection succeeds but execution fails.
Solution: Both use Dependency::Tool methods.
❌ Checking PATH Only
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
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.