From packaging-skills
Use when implementing dependency installation systems - covers bundled portable executables in MSI installers, unified detection/execution code paths, and Windows installer best practices
How this skill is triggered — by the user, by Claude, or both
Slash command
/packaging-skills:skills/robust-dependency-installationThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
> **See also:** For the full DMG/MSI/DEB packaging workflow, GitHub Actions release automation, and Homebrew updates, see the `build-cross-platform-packages` skill.
See also: For the full DMG/MSI/DEB packaging workflow, GitHub Actions release automation, and Homebrew updates, see the
build-cross-platform-packagesskill.
Use this skill when:
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:
Trade-offs:
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:
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.
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:
is_available()) uses same code as execution (create_command())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:
When bundling portable executables, you MUST comply with licenses:
Verify all licenses allow redistribution:
Create /LICENSES/ directory with license texts
Create /third_party/<dep>/NOTICE attribution files
For LGPL (FFmpeg): Include source link or code
Update README.md with third-party attributions
Create .gitignore in /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
target/bundled-deps/ before WiX build7z x tesseract-setup.exe -o deps/tesseract/autobuild-2024-11-04-12-55)# 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.
// 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.
// 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.
// 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().
From production deployment (bundled dependency architecture):
✅ Installation success rate: ~100% (up from ~90% with Scoop/Chocolatey)
✅ Instant dependency availability
✅ Version control
✅ Detection reliability
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.
Modern dependency installation uses bundled portable executables embedded in MSI installers.
Key principles:
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:
C:\Program Files\myapp\deps\)Result: ~100% installation success, instant availability, version control, no network dependency.
npx claudepluginhub securityronin/ronin-marketplace --plugin packaging-skillsPackaging MSIX apps. Creation, signing, Store submission, App Installer sideload, auto-update.
Packages a local web project as a Windows desktop app with a Start Menu shortcut, .exe launcher, and Taskbar identity. Mirrors macOS app-it contract (soft-close vs quit, warm dev-server reuse). Beta — requires Windows maintainer to verify hardware-dependent steps.
Builds production-grade Windows installers via Nuitka extreme compilation, dist slimming, DLL analysis, and Inno Setup packaging. Targets advanced size/startup optimization, not basic script-to-exe conversion.