Skip to content

Commit 36b4fd2

Browse files
Respect verbatim executable name in uvx (#11524)
## Summary If the user provides a PEP 508 requirement (e.g., `uvx change_wheel_version`), then we should us that verbatim for the executable, rather than normalizing the package name. Closes #11521.
1 parent 172305a commit 36b4fd2

File tree

2 files changed

+112
-3
lines changed

2 files changed

+112
-3
lines changed

crates/uv/src/commands/tool/run.rs

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -529,6 +529,22 @@ async fn get_or_create_environment(
529529
// Ex) `ruff>=0.6.0`
530530
Target::Unspecified(requirement) => {
531531
let spec = RequirementsSpecification::parse_package(requirement)?;
532+
533+
// Extract the verbatim executable name, if possible.
534+
let name = match &spec.requirement {
535+
UnresolvedRequirement::Named(..) => {
536+
// Identify the package name from the PEP 508 specifier.
537+
//
538+
// For example, given `ruff>=0.6.0`, extract `ruff`, to use as the executable name.
539+
let content = requirement.trim();
540+
let index = content
541+
.find(|c| !matches!(c, 'A'..='Z' | 'a'..='z' | '0'..='9' | '-' | '_' | '.'))
542+
.unwrap_or(content.len());
543+
Some(&content[..index])
544+
}
545+
UnresolvedRequirement::Unnamed(..) => None,
546+
};
547+
532548
if let UnresolvedRequirement::Named(requirement) = &spec.requirement {
533549
if requirement.name.as_str() == "python" {
534550
return Err(anyhow::anyhow!(
@@ -539,6 +555,7 @@ async fn get_or_create_environment(
539555
.into());
540556
}
541557
}
558+
542559
let requirement = resolve_names(
543560
vec![spec],
544561
&interpreter,
@@ -556,12 +573,14 @@ async fn get_or_create_environment(
556573
.pop()
557574
.unwrap();
558575

559-
// Use the executable provided by the user, if possible (as in: `uvx --from package executable`).
560-
//
561-
// If no such executable was provided, rely on the package name (as in: `uvx git+https://github.com/pallets/flask`).
576+
// Prefer, in order:
577+
// 1. The verbatim executable provided by the user, independent of the requirement (as in: `uvx --from package executable`).
578+
// 2. The verbatim executable provided by the user as a named requirement (as in: `uvx change_wheel_version`).
579+
// 3. The resolved package name (as in: `uvx git+https://github.com/pallets/flask`).
562580
let executable = request
563581
.executable
564582
.map(ToString::to_string)
583+
.or_else(|| name.map(ToString::to_string))
565584
.unwrap_or_else(|| requirement.name.to_string());
566585

567586
(executable, requirement)

crates/uv/tests/it/tool_run.rs

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1810,3 +1810,93 @@ fn tool_run_from_at() {
18101810
+ executable-application==0.2.0
18111811
"###);
18121812
}
1813+
1814+
#[test]
1815+
fn tool_run_verbatim_name() {
1816+
let context = TestContext::new("3.12")
1817+
.with_filtered_counts()
1818+
.with_filtered_exe_suffix();
1819+
let tool_dir = context.temp_dir.child("tools");
1820+
let bin_dir = context.temp_dir.child("bin");
1821+
1822+
// The normalized package name is `change-wheel-version`, but the executable is `change_wheel_version`.
1823+
uv_snapshot!(context.filters(), context.tool_run()
1824+
.arg("change_wheel_version")
1825+
.arg("--help")
1826+
.env(EnvVars::UV_TOOL_DIR, tool_dir.as_os_str())
1827+
.env(EnvVars::XDG_BIN_HOME, bin_dir.as_os_str()), @r###"
1828+
success: true
1829+
exit_code: 0
1830+
----- stdout -----
1831+
usage: change_wheel_version [-h] [--local-version LOCAL_VERSION]
1832+
[--version VERSION] [--delete-old-wheel]
1833+
[--allow-same-version]
1834+
wheel
1835+
1836+
positional arguments:
1837+
wheel
1838+
1839+
options:
1840+
-h, --help show this help message and exit
1841+
--local-version LOCAL_VERSION
1842+
--version VERSION
1843+
--delete-old-wheel
1844+
--allow-same-version
1845+
1846+
----- stderr -----
1847+
Resolved [N] packages in [TIME]
1848+
Prepared [N] packages in [TIME]
1849+
Installed [N] packages in [TIME]
1850+
+ change-wheel-version==0.5.0
1851+
+ installer==0.7.0
1852+
+ packaging==24.0
1853+
+ wheel==0.43.0
1854+
"###);
1855+
1856+
uv_snapshot!(context.filters(), context.tool_run()
1857+
.arg("change-wheel-version")
1858+
.arg("--help")
1859+
.env(EnvVars::UV_TOOL_DIR, tool_dir.as_os_str())
1860+
.env(EnvVars::XDG_BIN_HOME, bin_dir.as_os_str()), @r###"
1861+
success: false
1862+
exit_code: 1
1863+
----- stdout -----
1864+
The executable `change-wheel-version` was not found.
1865+
The following executables are provided by `change-wheel-version`:
1866+
- change_wheel_version
1867+
Consider using `uv tool run --from change-wheel-version <EXECUTABLE_NAME>` instead.
1868+
1869+
----- stderr -----
1870+
Resolved [N] packages in [TIME]
1871+
warning: An executable named `change-wheel-version` is not provided by package `change-wheel-version`.
1872+
"###);
1873+
1874+
uv_snapshot!(context.filters(), context.tool_run()
1875+
.arg("--from")
1876+
.arg("change-wheel-version")
1877+
.arg("change_wheel_version")
1878+
.arg("--help")
1879+
.env(EnvVars::UV_TOOL_DIR, tool_dir.as_os_str())
1880+
.env(EnvVars::XDG_BIN_HOME, bin_dir.as_os_str()), @r###"
1881+
success: true
1882+
exit_code: 0
1883+
----- stdout -----
1884+
usage: change_wheel_version [-h] [--local-version LOCAL_VERSION]
1885+
[--version VERSION] [--delete-old-wheel]
1886+
[--allow-same-version]
1887+
wheel
1888+
1889+
positional arguments:
1890+
wheel
1891+
1892+
options:
1893+
-h, --help show this help message and exit
1894+
--local-version LOCAL_VERSION
1895+
--version VERSION
1896+
--delete-old-wheel
1897+
--allow-same-version
1898+
1899+
----- stderr -----
1900+
Resolved [N] packages in [TIME]
1901+
"###);
1902+
}

0 commit comments

Comments
 (0)