| name | vx-provider-updater |
| description | Update existing VX providers to the latest standards: provider.star as single source of truth,
top-level variables for metadata (name = "..."), object-style ctx access (ctx.platform.os),
stdlib helpers (make_fetch_versions, github_asset_url, env_prepend), and required path query
functions (store_root, get_execute_path, post_install). Add package_alias for PyPI/npm tools
(RFC 0033: vx meson = vx uvx:meson, vx vite = vx npx:vite). All providers must follow the
current provider.star format with proper system_install {"strategies": [...]} structure.
|
VX Provider Updater
Migrate all VX providers to the latest standards: provider.star as the single source of truth,
replacing provider.toml entirely.
When to Use
- Migrating from old function-based metadata to top-level variables (
def name(): return "..." → name = "...")
- Migrating from
ctx["platform"]["os"] to ctx.platform.os (object-style access)
- Migrating from
make_github_provider to make_fetch_versions + github_asset_url
- Migrating
environment() from dict return to list of env_prepend()/env_set() calls
- Adding required path query functions (
store_root, get_execute_path, post_install)
- Adding
package_alias for PyPI/npm tools (RFC 0033: vx meson = vx uvx:meson)
- Fixing
system_install format to use {"strategies": [...]} structure
- Fixing
runtimes list to use runtime_def() / bundled_runtime_def() function call format (required for StarMetadata::parse() to detect runtimes correctly)
- Standardizing provider manifests
- Fixing download/installation issues
- Batch updating multiple providers
Core Changes Summary
Change 1: Metadata as Top-Level Variables (NOT functions)
OLD (forbidden):
def name():
return "mytool"
def description():
return "My awesome tool"
def ecosystem():
return "custom"
NEW (required):
name = "mytool"
description = "My awesome tool"
ecosystem = "custom"
Change 2: ctx Object Access (NOT dict access)
OLD (forbidden):
os = ctx["platform"]["os"]
arch = ctx["platform"]["arch"]
releases = ctx["http"]["get_json"]("https://...")
NEW (required):
os = ctx.platform.os
arch = ctx.platform.arch
releases = ctx.http.get_json("https://...")
Change 3: New stdlib Helpers
OLD (less preferred):
_p = make_github_provider("owner", "repo", "tool-{triple}.{ext}")
fetch_versions = _p["fetch_versions"]
download_url = _p["download_url"]
NEW (preferred):
load("@vx//stdlib:github.star", "make_fetch_versions", "github_asset_url")
fetch_versions = make_fetch_versions("owner", "repo")
def download_url(ctx, version):
asset = "tool-{}-{}.{}".format(version, triple, ext)
return github_asset_url("owner", "repo", "v" + version, asset)
Change 4: Remove Redundant Old Functions
Remove these old functions (replaced by new API):
post_extract(ctx, version, install_dir) — merge into post_install
Required / recommended functions in current format:
fetch_versions(ctx) — required (or use make_fetch_versions)
download_url(ctx, version) — strongly recommended
install_layout(ctx, version) — optional (has defaults)
environment(ctx, version) — optional, returns list of env_prepend()/env_set() calls
post_install(ctx, version) — optional, return None if nothing to do
store_root(ctx) — optional, return path to store root
get_execute_path(ctx, version) — optional, return path to executable
system_install(ctx) — optional, return {"strategies": [...]} for PM fallback
uninstall(ctx, version) — optional, return uninstall descriptor or False
deps(ctx, version) — optional, return list of dependency dicts
Change 5: pre_run Signature Change
OLD:
def pre_run(ctx, args, executable):
...
NEW:
def pre_run(ctx, args):
...
Change 6: environment() Returns List (NOT dict)
OLD (forbidden):
def environment(ctx, version, install_dir):
return {"PATH": install_dir}
NEW (required):
load("@vx//stdlib:env.star", "env_prepend", "env_set")
def environment(ctx, _version):
return [
env_prepend("PATH", ctx.install_dir),
]
Change 7: system_install Returns {"strategies": [...]}
OLD (forbidden):
"system_install": [
{"manager": "brew", "package": "mytool"},
]
NEW (required):
def system_install(ctx):
os = ctx.platform.os
if os == "windows":
return {
"strategies": [
{"manager": "winget", "package": "Publisher.MyTool", "priority": 95},
{"manager": "choco", "package": "mytool", "priority": 80},
],
}
elif os == "macos":
return {
"strategies": [
{"manager": "brew", "package": "mytool", "priority": 90},
],
}
return {}
Change 8: Add Required Path Query Functions
All providers must implement these functions:
def store_root(ctx):
return ctx.vx_home + "/store/{name}"
def get_execute_path(ctx, version):
os = ctx.platform.os
exe = "{name}.exe" if os == "windows" else "{name}"
return ctx.install_dir + "/" + exe
def post_install(_ctx, _version):
return None
Change 9: deps() Returns List of Dicts
def deps(_ctx, _version):
return [
{"runtime": "node", "version": ">=18",
"reason": "Requires Node.js runtime"},
]
License Field Requirement
All providers MUST have license as a top-level variable:
license = "MIT"
Blocked licenses (AGPL-3.0, SSPL, CC BY-NC) must NOT be integrated as providers.
Quick Reference
Tool Categories
| Category | Layout Type | Examples |
|---|
| Single Binary | binary | kubectl, ninja, rust |
| Standard Archive (bin/) | archive + strip | node, go, cmake |
| Root Directory | archive (no strip) | terraform, just, deno |
| Platform Directory | archive + platform strip | helm, bun |
| Binary + Registry Clone | binary + git clone | vcpkg (downloads binary, clones registry) |
| Hybrid (Download + PM) | binary/archive + system_install | imagemagick, ffmpeg, docker |
| npm/pip Packages | No layout needed | vite, pre-commit |
| System Tools | Detection only | git, docker, openssl |
| System PM Only | system_install only | make, curl, openssl |
Update Templates
All provider logic now lives in provider.star. See references/update-templates.md for
complete migration templates covering:
- Standard GitHub Provider —
make_fetch_versions + custom download_url
- Hybrid Provider — direct download on some platforms +
system_install fallback
- PyPI/npm Package Alias —
package_alias for ecosystem-managed tools
- MSI on Windows —
msi_install() + archive_install() per platform
- Non-GitHub Version Source — custom
fetch_versions with fetch_json_versions
The provider.toml now only contains metadata (name, description, ecosystem, license).
All install logic (download URLs, archive layout, system_install, environment) belongs in provider.star.
Update Workflow
Step 1: Identify Tool Type
cat crates/vx-providers/{name}/provider.star
Questions to answer:
- Does it use old function-based metadata (
def name(): return "...")?
- Does it use old dict-style ctx access (
ctx["platform"]["os"])?
- Does
environment() return a dict instead of a list?
- Are
store_root, get_execute_path, post_install missing?
- Does
system_install return a flat list instead of {"strategies": [...]}?
Step 2: Choose Migration Path
Is provider.star using old format?
├─ Old function metadata → Change 1: top-level variables
├─ ctx["platform"]["os"] → Change 2: ctx.platform.os
├─ make_github_provider → Change 3: make_fetch_versions + github_asset_url
├─ environment() returns dict → Change 6: return list with env_prepend()
├─ system_install flat list → Change 7: {"strategies": [...]}
└─ Missing store_root/get_execute_path/post_install → Change 8: add them
See references/update-templates.md for complete before/after examples.
Step 3: Apply Changes
- Open
crates/vx-providers/{name}/provider.star
- Apply the relevant changes from the Core Changes Summary above
- Ensure
provider.toml only contains metadata (no layout fields)
- Save files
Step 4: Verify Format
Checklist:
Step 5: Test
cargo build --release
vx install {name}@{version}
vx which {name}
vx {name} --version
Migration: From Rust runtime.rs to Starlark provider.star
Starlark (provider.star) is the single source of truth for all provider metadata and
install logic. Every provider crate — whether it keeps custom Rust code or is fully
manifest-driven — must embed provider.star at compile time and expose its metadata
through star_metadata().
Architecture Overview
provider.star (single source of truth)
│
│ include_str!("../provider.star") ← compile-time embed
│ build.rs watches for changes
▼
lib.rs
├── pub const PROVIDER_STAR: &str = include_str!("../provider.star")
└── pub fn star_metadata() -> &'static StarMetadata ← OnceLock lazy parse
│
▼
provider.rs / runtime.rs
├── name() → star_metadata().name_or("tool")
├── description() → star_metadata().description (OnceLock &'static str)
├── aliases() → star_metadata().runtimes[0].aliases
├── metadata() → star_metadata().homepage / repository / license
└── supports() → star_metadata().runtimes[*].aliases
When to Migrate
- Provider has custom
download_url() logic in config.rs
- Provider has
post_extract() or install() overrides in runtime.rs
- Provider needs MSI install support on Windows
- Provider needs system package manager fallback
- Any new provider being created
- Any existing provider that still hardcodes name/description in Rust
Migration Steps
Step 0: Add build.rs (ALL providers)
Every provider crate must have a build.rs that watches provider.star:
fn main() {
println!("cargo:rerun-if-changed=provider.star");
}
Step 0b: Update lib.rs (ALL providers)
Every provider crate's lib.rs must embed provider.star and expose star_metadata():
pub const PROVIDER_STAR: &str = include_str!("../provider.star");
pub fn star_metadata() -> &'static vx_starlark::StarMetadata {
use std::sync::OnceLock;
static META: OnceLock<vx_starlark::StarMetadata> = OnceLock::new();
META.get_or_init(|| vx_starlark::StarMetadata::parse(PROVIDER_STAR))
}
Also add vx-starlark to Cargo.toml:
[dependencies]
vx-runtime = { workspace = true }
vx-starlark = { workspace = true }
Step 0c: Update provider.rs / runtime.rs (ALL providers with custom Rust)
Replace hardcoded strings with calls to star_metadata():
impl Provider for MyProvider {
fn name(&self) -> &str {
crate::star_metadata().name_or("mytool")
}
fn description(&self) -> &str {
use std::sync::OnceLock;
static DESC: OnceLock<&'static str> = OnceLock::new();
DESC.get_or_init(|| {
let s = crate::star_metadata()
.description
.as_deref()
.unwrap_or("My tool description");
Box::leak(s.to_string().into_boxed_str())
})
}
fn supports(&self, name: &str) -> bool {
if name == self.name() { return true; }
crate::star_metadata()
.runtimes
.iter()
.any(|r| r.name.as_deref() == Some(name) || r.aliases.iter().any(|a| a == name))
}
}
impl Runtime for MyRuntime {
fn aliases(&self) -> &[&str] {
use std::sync::OnceLock;
static ALIASES: OnceLock<Vec<&'static str>> = OnceLock::new();
ALIASES.get_or_init(|| {
let meta = crate::star_metadata();
if let Some(rt) = meta.runtimes.iter().find(|r| r.name.as_deref() == Some("mytool")) {
rt.aliases
.iter()
.map(|a| Box::leak(a.clone().into_boxed_str()) as &'static str)
.collect()
} else {
vec![]
}
})
}
fn metadata(&self) -> HashMap<String, String> {
let mut meta = HashMap::new();
let star = crate::star_metadata();
if let Some(hp) = star.homepage.as_deref() {
meta.insert("homepage".to_string(), hp.to_string());
}
if let Some(repo) = star.repository.as_deref() {
meta.insert("repository".to_string(), repo.to_string());
}
if let Some(license) = star.license.as_deref() {
meta.insert("license".to_string(), license.to_string());
}
meta
}
}
Step 1: Create provider.star
Create crates/vx-providers/{name}/provider.star with the equivalent logic:
Before (Rust config.rs):
pub fn download_url(version: &str, platform: &Platform) -> Option<String> {
let triple = match (&platform.os, &platform.arch) {
(Os::Windows, Arch::X86_64) => "x86_64-pc-windows-msvc",
(Os::MacOS, Arch::Aarch64) => "aarch64-apple-darwin",
(Os::Linux, Arch::X86_64) => "x86_64-unknown-linux-musl",
_ => return None,
};
let ext = if platform.os == Os::Windows { "zip" } else { "tar.gz" };
Some(format!("https://github.com/owner/repo/releases/download/v{}/tool-v{}-{}.{}",
version, version, triple, ext))
}
After (Starlark provider.star):
load("@vx//stdlib:github.star", "make_fetch_versions", "github_asset_url")
load("@vx//stdlib:env.star", "env_prepend")
fetch_versions = make_fetch_versions("owner", "repo")
def _triple(ctx):
os = ctx.platform.os
arch = ctx.platform.arch
return {
"windows/x64": "x86_64-pc-windows-msvc",
"macos/arm64": "aarch64-apple-darwin",
"linux/x64": "x86_64-unknown-linux-musl",
}.get("{}/{}".format(os, arch))
def download_url(ctx, version):
triple = _triple(ctx)
if not triple:
return None
os = ctx.platform.os
ext = "zip" if os == "windows" else "tar.gz"
asset = "tool-v{}-{}.{}".format(version, triple, ext)
return github_asset_url("owner", "repo", "v" + version, asset)
def install_layout(ctx, version):
os = ctx.platform.os
exe = "tool.exe" if os == "windows" else "tool"
return {
"type": "archive",
"strip_prefix": "tool-v{}-{}".format(version, _triple(ctx) or ""),
"executable_paths": [exe, "tool"],
}
def environment(ctx, _version):
return [env_prepend("PATH", ctx.install_dir)]
def store_root(ctx):
return ctx.vx_home + "/store/tool"
def get_execute_path(ctx, version):
os = ctx.platform.os
exe = "tool.exe" if os == "windows" else "tool"
return ctx.install_dir + "/" + exe
def post_install(_ctx, _version):
return None
def deps(_ctx, _version):
return []
Step 2: Simplify provider.toml
After creating provider.star, the provider.toml only needs metadata (remove layout fields):
[provider]
name = "mytool"
description = "My awesome tool"
homepage = "https://example.com"
repository = "https://github.com/owner/repo"
ecosystem = "devtools"
license = "MIT"
Step 3: Remove Rust files (if manifest-only)
For providers that use ManifestDrivenRuntime (i.e., provider.rs delegates entirely to
the manifest), delete the now-redundant files:
rm crates/vx-providers/{name}/src/runtime.rs
rm crates/vx-providers/{name}/src/config.rs
For providers with custom Rust logic (brew, choco, make, msbuild, msvc, winget), keep
runtime.rs and config.rs but update them to read metadata from star_metadata() as
shown in Step 0c above.
Current status (as of this migration):
- ✅ 49 providers: fully manifest-driven (
ManifestDrivenRuntime), runtime.rs/config.rs deleted
- ✅ 6 providers: custom Rust kept, metadata sourced from
provider.star via star_metadata()
- ✅ All 55 providers: have
build.rs, PROVIDER_STAR constant, and star_metadata() function
Starlark Standard Library Quick Reference
| Module | Load Path | Key Functions |
|---|
| GitHub | @vx//stdlib:github.star | make_fetch_versions(owner, repo), make_download_url(owner, repo, template), make_github_provider(owner, repo, template), github_asset_url(owner, repo, tag, asset) |
| Platform | @vx//stdlib:platform.star | is_windows(ctx), is_macos(ctx), is_linux(ctx), is_x64(ctx), is_arm64(ctx), platform_triple(ctx), platform_ext(ctx), exe_ext(ctx), arch_to_gnu(arch), arch_to_go(arch), os_to_go(os) |
| Install | @vx//stdlib:install.star | msi_install(url, ...), archive_install(url, ...), binary_install(url, ...), platform_install(ctx, ...) |
| Env | @vx//stdlib:env.star | env_set(key, value), env_prepend(key, value), env_append(key, value), env_unset(key) |
| HTTP | @vx//stdlib:http.star | github_releases(ctx, owner, repo), fetch_json_versions(ctx, url, transform), releases_to_versions(releases) |
| Semver | @vx//stdlib:semver.star | semver_compare(a, b), semver_gt/lt/gte/lte/eq(a, b), semver_sort(versions), semver_strip_v(v) |
ctx Object Reference
ctx.platform.os
ctx.platform.arch
ctx.platform.target
ctx.install_dir
ctx.vx_home
ctx.version
install_layout Return Values
| Type | Required Fields | Optional Fields |
|---|
"archive" | type | strip_prefix, executable_paths |
"binary" | type | executable_name, source_name, permissions |
"msi" | type, url | executable_paths, strip_prefix, extra_args |
Migration: Adding MSI Install Support (Windows)
For tools that distribute .msi installers on Windows, use msi_install() from install.star.
How MSI Install Works
The Rust runtime runs:
msiexec /a <file.msi> /qn /norestart TARGETDIR=<install_dir>
This extracts the MSI contents to install_dir without modifying the Windows registry.
Template: MSI on Windows + Archive on Other Platforms
load("@vx//stdlib:install.star", "msi_install", "archive_install")
load("@vx//stdlib:github.star", "make_fetch_versions", "github_asset_url")
fetch_versions = make_fetch_versions("owner", "repo")
def download_url(ctx, version):
os = ctx.platform.os
if os == "windows":
return "https://github.com/owner/repo/releases/download/v{}/tool-{}-x64.msi".format(version, version)
elif os == "macos":
return github_asset_url("owner", "repo", "v" + version, "tool-{}-macos.tar.gz".format(version))
elif os == "linux":
return github_asset_url("owner", "repo", "v" + version, "tool-{}-linux.tar.gz".format(version))
return None
def install_layout(ctx, version):
os = ctx.platform.os
url = download_url(ctx, version)
if os == "windows":
return msi_install(
url,
executable_paths = ["bin/tool.exe", "tool.exe"],
)
else:
return archive_install(
url,
strip_prefix = "tool-{}".format(version),
executable_paths = ["bin/tool"],
)
def environment(ctx, _version):
return [env_prepend("PATH", ctx.install_dir)]
def store_root(ctx):
return ctx.vx_home + "/store/tool"
def get_execute_path(ctx, version):
os = ctx.platform.os
exe = "tool.exe" if os == "windows" else "tool"
return ctx.install_dir + "/" + exe
def post_install(_ctx, _version):
return None
def deps(_ctx, _version):
return []
Template: platform_install() Convenience Helper
load("@vx//stdlib:install.star", "platform_install")
def install_layout(ctx, version):
return platform_install(
ctx,
windows_url = "https://example.com/tool-{}.msi".format(version),
macos_url = "https://example.com/tool-{}-macos.tar.gz".format(version),
linux_url = "https://example.com/tool-{}-linux.tar.gz".format(version),
windows_msi = True,
executable_paths = ["bin/tool.exe", "bin/tool"],
strip_prefix = "tool-{}".format(version),
)
Migration: Adding System Package Manager Fallback
When a tool has "No download URL" errors on certain platforms, add package manager fallback.
Identify the Problem
Error messages indicating need for package manager fallback:
No download URL for {tool} {version} on macOS/Windows
Executable not found: ~/.vx/store/{tool}/{version}/bin/{tool} after "successful" install
- Tool works on Linux but fails on macOS/Windows
Step 1: Check Platform Coverage
grep -A 5 "download_url" crates/vx-providers/{name}/provider.star
Step 2: Add system_install() to provider.star
def system_install(ctx):
os = ctx.platform.os
if os == "windows":
return {
"strategies": [
{"manager": "winget", "package": "Publisher.Package", "priority": 95},
{"manager": "choco", "package": "{tool}", "priority": 80},
{"manager": "scoop", "package": "{tool}", "priority": 60},
],
}
elif os == "macos":
return {
"strategies": [
{"manager": "brew", "package": "{tool}", "priority": 90},
],
}
elif os == "linux":
return {
"strategies": [
{"manager": "apt", "package": "{tool}", "priority": 80},
{"manager": "dnf", "package": "{tool}", "priority": 80},
],
}
return {}
Step 3: Update runtime.rs (for custom Runtime implementations)
If the provider has a custom runtime.rs (not manifest-driven), add:
use vx_system_pm::{PackageInstallSpec, PackageManagerRegistry};
use vx_runtime::InstallResult;
async fn install(&self, version: &str, ctx: &RuntimeContext) -> Result<InstallResult> {
let platform = Platform::current();
if let Some(url) = self.download_url(version, &platform).await? {
return self.install_via_download(version, &url, ctx).await;
}
self.install_via_package_manager(version, ctx).await
}
async fn install_via_package_manager(
&self,
version: &str,
_ctx: &RuntimeContext,
) -> Result<InstallResult> {
let registry = PackageManagerRegistry::new();
let available = registry.get_available().await;
for pm in &available {
let package = Self::get_package_name_for_manager(pm.name());
let spec = PackageInstallSpec {
package: package.to_string(),
..Default::default()
};
if pm.install_package(&spec).await.is_ok() {
let exe_path = which::which("{executable}").ok();
return Ok(InstallResult::system_installed(
format!("{} (via {})", version, pm.name()),
exe_path,
));
}
}
Err(anyhow::anyhow!("No package manager available"))
}
Step 5: Add Cargo.toml Dependencies
[dependencies]
vx-system-pm = { workspace = true }
tracing = { workspace = true }
which = { workspace = true }
Package Name Lookup
| Manager | How to Find Package Name |
|---|
| brew | brew search {tool} |
| winget | winget search {tool} |
| choco | choco search {tool} |
| scoop | scoop search {tool} |
| apt | apt search {tool} |
| dnf | dnf search {tool} |
Common Package Name Mappings
| Tool | brew | winget | choco | Notes |
|---|
| ImageMagick | imagemagick | ImageMagick.ImageMagick | imagemagick | |
| FFmpeg | ffmpeg | Gyan.FFmpeg | ffmpeg | |
| AWS CLI | awscli | Amazon.AWSCLI | awscli | |
| Azure CLI | azure-cli | Microsoft.AzureCLI | azure-cli | |
| Docker | docker | Docker.DockerDesktop | docker-desktop | Desktop on Win/Mac |
| Git | git | Git.Git | git | |
| Make | make | GnuWin32.Make | make | |
Special Cases
Case 1: Installer Files (.msi, .pkg)
For tools using installers (not supported yet):
[runtimes.platforms.windows]
executable_extensions = [".exe"]
Examples: awscli, azcli, gcloud, ollama, vscode
Case 2: Multiple Executables
For tools providing multiple executables:
[runtimes.layout.archive]
strip_prefix = "toolset-{version}"
executable_paths = [
"bin/tool1.exe",
"bin/tool1",
"bin/tool2.exe",
"bin/tool2"
]
Case 3: Bundled Dependencies
Tools bundled with another runtime:
[[runtimes]]
name = "npm"
executable = "npm"
bundled_with = "node"
[[runtimes.constraints]]
when = "*"
requires = [
{ runtime = "node", version = "*", reason = "npm is bundled with Node.js" }
]
Validation Rules
Rule 1: Platform Coverage
Binary layout must cover all supported platforms:
- ✅ windows-x86_64
- ✅ macos-x86_64, macos-aarch64
- ✅ linux-x86_64, linux-aarch64
Rule 2: Unix Permissions
Unix platforms must have target_permissions:
[runtimes.layout.binary."linux-x86_64"]
source_name = "tool"
target_name = "tool"
target_dir = "bin"
[runtimes.layout.binary."linux-x86_64"]
source_name = "tool"
target_name = "tool"
target_dir = "bin"
target_permissions = "755"
Rule 3: Path Separators
Always use forward slashes in paths:
executable_paths = ["bin\\tool.exe"]
executable_paths = ["bin/tool.exe"]
Rule 4: Variable Syntax
Use correct variable placeholders:
strip_prefix = "$version-{os}"
strip_prefix = "{version}-{os}"
Testing Checklist
After updating a provider:
Update Documentation
After successful update:
- Update migration status in
docs/provider-migration-status.md
- Add entry to changelog if significant
- Update tool documentation in
docs/tools/ if needed
Troubleshooting
Issue: Executable not found after install
Cause: Incorrect strip_prefix or executable_paths
Solution:
- Download the archive manually
- Inspect the actual structure
- Update configuration to match
Issue: Permission denied on Unix
Cause: Missing target_permissions
Solution: Add target_permissions = "755" to Unix platforms
Issue: Wrong executable on Windows
Cause: Incorrect file extension or order in executable_paths
Solution: Ensure .exe files come before non-extension files
Issue: Version variable not replaced
Cause: Wrong variable syntax or missing variable
Solution: Use {version} not $version or ${version}
Issue: "No download URL for {tool}" on macOS/Windows
Cause: No layout.binary configuration for that platform, and no package manager fallback
Solution: Add system package manager support:
- Add
system_deps.pre_depends for brew (macOS) or winget/choco (Windows)
- Add
system_install.strategies for each package manager
- If custom runtime.rs, implement
install() with package manager fallback
Issue: "Executable not found" after system package manager install
Cause: install_quiet returns version string, loses executable_path from InstallResult
Solution:
- Ensure
install() returns InstallResult::system_installed(version, Some(exe_path))
- Use
which::which("{executable}") to get actual executable path
- Test handler should use
executable_path from InstallResult, not compute store path
Issue: Package manager install succeeds but tool not found
Cause: Package manager installed to non-standard location, or which::which() fails
Solution:
- Check package manager's installation location
- Ensure PATH includes package manager's bin directory
- Use explicit path lookup:
which::which("{executable}")
Issue: Wrong package name for package manager
Cause: Package names differ between package managers
Solution: Look up correct package name for each manager:
- brew:
brew search {tool}
- winget:
winget search {tool} (uses Publisher.Package format)
- choco:
choco search {tool}
- scoop:
scoop search {tool}
Issue: TOML parse error "unknown variant" for download_type
Cause: Using kebab-case (git-clone) instead of snake_case (git_clone)
Solution: Use snake_case for all enum values in provider.toml:
download_type = "git_clone" ✅
download_type = "git-clone" ❌
ecosystem = "cpp" ✅ (new in recent update)
The error diagnostic system will suggest the correct format.
Issue: TOML parse error "unknown variant" for ecosystem
Cause: Using an unsupported ecosystem value
Solution: Use one of the supported values:
nodejs, python, rust, go, ruby, java, dotnet, devtools, container, cloud, ai, cpp, zig, system
Issue: "No factory registered" appears in debug output
Cause: Provider has provider.toml but no Rust factory implementation
Solution: This is expected for manifest-only providers (in development). The build summary now shows:
registered 53 lazy providers (0 errors, 9 manifest-only, 0 warnings)
- errors: Real configuration problems
- manifest-only: Expected for providers without Rust implementation yet
Issue: vx <tool> reports "Tool 'X' is not supported by vx" even though provider exists
Root Cause: StarMetadata::parse() is a static parser — it reads provider.star without
executing Starlark. It only recognizes two formats for the runtimes = [...] list:
- Dict literals:
{"name": "foo", ...}
- Function calls:
runtime_def("foo", ...) and bundled_runtime_def("foo", bundled_with="bar", ...)
If the runtimes list uses any other format (e.g., a helper function that returns a list,
or a variable reference), the static parser will see an empty runtimes list, causing
ProviderRegistry::get_runtime("tool") to return None.
Diagnosis:
cargo test -p vx-starlark --lib -- test_parse_node_provider_star 2>&1
Solution: Ensure the top-level runtimes = [...] list uses one of the two supported formats:
runtimes = [
{"name": "mytool", "executable": "mytool", "aliases": ["mt"]},
{"name": "mytool-extra", "bundled_with": "mytool"},
]
runtimes = [
runtime_def("mytool",
aliases = ["mt"],
),
bundled_runtime_def("mytool-extra", bundled_with = "mytool"),
]
runtimes = make_runtimes()
_rt = [{"name": "mytool"}]
runtimes = _rt
Note: Both runtime_def() and bundled_runtime_def() are parsed by StarMetadata::parse()
in crates/vx-starlark/src/metadata.rs. The parser extracts:
runtime_def("name", aliases=[...], executable="...", description="...") → StarRuntimeMeta
bundled_runtime_def("name", bundled_with="parent", ...) → StarRuntimeMeta with bundled_with set
Migration: Adding package_alias for Ecosystem-Managed Tools (RFC 0033)
Use package_alias when a tool is distributed as a package in an ecosystem (PyPI via uvx,
or npm via npx) rather than as a standalone binary. This routes vx <name> to
vx <ecosystem>:<package>, giving each version its own isolated environment.
When to Use
| Tool Type | Example | package_alias |
|---|
| Python CLI tool (PyPI) | meson, ruff, black, mypy, nox | {"ecosystem": "uvx", "package": "..."} |
| npm CLI tool | vite, eslint, prettier, create-react-app | {"ecosystem": "npx", "package": "..."} |
How It Works
vx meson@1.5.0
↓ RFC 0033: package_alias routing (from provider.star)
vx uvx:meson@1.5.0
↓ UvxInstaller.install()
uv tool install meson==1.5.0 (pre-warms uv cache)
+ creates shim: exec uvx meson==1.5.0 "$@"
↓ execution
uvx meson==1.5.0 [args...] ← isolated Python env per version
Step 1: Add package_alias to provider.star
name = "meson"
description = "Meson - An extremely fast and user friendly build system"
homepage = "https://mesonbuild.com"
repository = "https://github.com/mesonbuild/meson"
license = "Apache-2.0"
ecosystem = "python"
aliases = ["mesonbuild"]
package_alias = {"ecosystem": "uvx", "package": "meson"}
runtimes = [
{
"name": "meson",
"executable": "meson",
"description": "Meson build system",
"aliases": ["mesonbuild"],
"priority": 100,
},
]
permissions = {
"http": ["pypi.org"],
"fs": [],
"exec": ["uvx", "uv"],
}
def download_url(_ctx, _version):
return None
def deps(_ctx, version):
return [
{"runtime": "uv", "version": "*",
"reason": "Tool is installed and run via uv"},
]
Step 2: Simplify provider.toml (metadata only)
When using package_alias, the provider.toml only needs metadata — no layout config needed:
[provider]
name = "meson"
description = "Meson - An extremely fast and user friendly build system"
homepage = "https://mesonbuild.com"
repository = "https://github.com/mesonbuild/meson"
ecosystem = "python"
license = "Apache-2.0"
Step 3: Remove layout configuration
If the provider previously had layout config (binary/archive), remove it — package_alias
tools don't download binaries directly.
Ecosystem Comparison
| Syntax | Equivalent | Installer | Runtime Dep | Isolation |
|---|
vx meson@1.5.0 | vx uvx:meson@1.5.0 | UvxInstaller | uv | Per-version Python env |
vx ruff@0.9.0 | vx uvx:ruff@0.9.0 | UvxInstaller | uv | Per-version Python env |
vx vite@5.0 | vx npx:vite@5.0 | NpmInstaller | node | npm cache |
vx yarn@1.22 | vx npm:yarn@1.22 | NpmInstaller | node | npm cache |
Troubleshooting: package_alias Not Working
Issue: vx meson still tries to download a binary instead of routing to uvx:meson
Cause: package_alias field not parsed from provider.star into StarMetadata
Check:
- Verify
package_alias = {"ecosystem": "uvx", "package": "meson"} is a top-level variable in provider.star (not inside a function)
- Verify
StarMetadata::parse() reads package_alias (check vx-starlark/src/metadata.rs)
- Verify
parse_metadata() in vx-starlark/src/provider/mod.rs maps star_meta.package_alias to ProviderMeta.package_alias
Issue: vx uvx:ruff@0.9.0 fails with "Unknown runtime 'uvx:ruff'"
Cause: uvx ecosystem not registered in the installer or runtime dependency maps
Check (all three must be present):
vx-ecosystem-pm/src/lib.rs: "uvx" => Ok(Box::new(UvxInstaller::new()))
vx-cli/src/lib.rs → get_all_required_runtimes_for_ecosystem("uvx"): returns vec!["uv"]
vx-shim/src/executor.rs → infer_all_runtimes_from_ecosystem("uvx"): returns vec![RuntimeDependency { runtime: "uv", ... }]
Reference
See also:
docs/provider-migration-status.md - Migration status tracker
docs/provider-update-summary.md - Batch update summary
crates/vx-providers/imagemagick/ - Example hybrid provider with PM fallback
crates/vx-providers/meson/ - Example package_alias provider (uvx ecosystem)