diff --git a/Cargo.lock b/Cargo.lock index 3a8e8184e2dfa..198ac0923b599 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5485,6 +5485,7 @@ dependencies = [ "uv-normalize", "uv-pep508", "uv-pypi-types", + "uv-warnings", ] [[package]] diff --git a/crates/uv-requirements-txt/Cargo.toml b/crates/uv-requirements-txt/Cargo.toml index 11e7d16668665..7dcc2964ecd2d 100644 --- a/crates/uv-requirements-txt/Cargo.toml +++ b/crates/uv-requirements-txt/Cargo.toml @@ -16,13 +16,14 @@ doctest = false workspace = true [dependencies] -uv-distribution-types = { workspace = true } -uv-pep508 = { workspace = true } -uv-pypi-types = { workspace = true } uv-client = { workspace = true } +uv-configuration = { workspace = true } +uv-distribution-types = { workspace = true } uv-fs = { workspace = true } uv-normalize = { workspace = true } -uv-configuration = { workspace = true } +uv-pep508 = { workspace = true } +uv-pypi-types = { workspace = true } +uv-warnings = { workspace = true } fs-err = { workspace = true } regex = { workspace = true } diff --git a/crates/uv-requirements-txt/src/lib.rs b/crates/uv-requirements-txt/src/lib.rs index 3c8264d53e9d0..97413d244dc97 100644 --- a/crates/uv-requirements-txt/src/lib.rs +++ b/crates/uv-requirements-txt/src/lib.rs @@ -88,6 +88,8 @@ enum RequirementsTxtStatement { NoBinary(NoBinary), /// `--only-binary` OnlyBinary(NoBuild), + /// An unsupported option (e.g., `--trusted-host`). + UnsupportedOption(UnsupportedOption), } /// A [Requirement] with additional metadata from the `requirements.txt`, currently only hashes but in @@ -384,6 +386,28 @@ impl RequirementsTxt { RequirementsTxtStatement::OnlyBinary(only_binary) => { data.only_binary.extend(only_binary); } + RequirementsTxtStatement::UnsupportedOption(flag) => { + if requirements_txt == Path::new("-") { + if flag.cli() { + uv_warnings::warn_user!("Ignoring unsupported option from stdin: {flag} (hint: pass {flag} on the command line instead)", flag = format!("`{flag}`").green()); + } else { + uv_warnings::warn_user!( + "Ignoring unsupported option from stdin: {flag}", + flag = format!("`{flag}`").green() + ); + } + } else { + if flag.cli() { + uv_warnings::warn_user!("Ignoring unsupported option in `{path}`: {flag} (hint: pass {flag} on the command line instead)", path = requirements_txt.user_display().cyan(), flag = format!("`{flag}`").green()); + } else { + uv_warnings::warn_user!( + "Ignoring unsupported option in `{path}`: {flag}", + path = requirements_txt.user_display().cyan(), + flag = format!("`{flag}`").green() + ); + } + } + } } } Ok(data) @@ -416,15 +440,70 @@ impl RequirementsTxt { } } +/// An unsupported option (e.g., `--trusted-host`). +/// +/// See: +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum UnsupportedOption { + PreferBinary, + RequireHashes, + Pre, + TrustedHost, + UseFeature, +} + +impl UnsupportedOption { + /// The name of the unsupported option. + fn name(self) -> &'static str { + match self { + UnsupportedOption::PreferBinary => "--prefer-binary", + UnsupportedOption::RequireHashes => "--require-hashes", + UnsupportedOption::Pre => "--pre", + UnsupportedOption::TrustedHost => "--trusted-host", + UnsupportedOption::UseFeature => "--use-feature", + } + } + + /// Returns `true` if the option is supported on the CLI. + fn cli(self) -> bool { + match self { + UnsupportedOption::PreferBinary => false, + UnsupportedOption::RequireHashes => true, + UnsupportedOption::Pre => true, + UnsupportedOption::TrustedHost => true, + UnsupportedOption::UseFeature => false, + } + } + + /// Returns an iterator over all unsupported options. + fn iter() -> impl Iterator { + [ + UnsupportedOption::PreferBinary, + UnsupportedOption::RequireHashes, + UnsupportedOption::Pre, + UnsupportedOption::TrustedHost, + UnsupportedOption::UseFeature, + ] + .iter() + .copied() + } +} + +impl Display for UnsupportedOption { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.name()) + } +} + /// Returns `true` if the character is a newline or a comment character. const fn is_terminal(c: char) -> bool { matches!(c, '\n' | '\r' | '#') } -/// Parse a single entry, that is a requirement, an inclusion or a comment line +/// Parse a single entry, that is a requirement, an inclusion or a comment line. /// -/// Consumes all preceding trivia (whitespace and comments). If it returns None, we've reached -/// the end of file +/// Consumes all preceding trivia (whitespace and comments). If it returns `None`, we've reached +/// the end of file. fn parse_entry( s: &mut Scanner, content: &str, @@ -595,14 +674,20 @@ fn parse_entry( hashes, }) } else if let Some(char) = s.peek() { - let (line, column) = calculate_row_column(content, s.cursor()); - return Err(RequirementsTxtParserError::Parser { - message: format!( - "Unexpected '{char}', expected '-c', '-e', '-r' or the start of a requirement" - ), - line, - column, - }); + // Identify an unsupported option, like `--trusted-host`. + if let Some(option) = UnsupportedOption::iter().find(|option| s.eat_if(option.name())) { + s.eat_while(|c: char| !is_terminal(c)); + RequirementsTxtStatement::UnsupportedOption(option) + } else { + let (line, column) = calculate_row_column(content, s.cursor()); + return Err(RequirementsTxtParserError::Parser { + message: format!( + "Unexpected '{char}', expected '-c', '-e', '-r' or the start of a requirement" + ), + line, + column, + }); + } } else { // EOF return Ok(None); diff --git a/crates/uv/tests/it/pip_install.rs b/crates/uv/tests/it/pip_install.rs index cdd067ee5c85e..789ce3835e4d2 100644 --- a/crates/uv/tests/it/pip_install.rs +++ b/crates/uv/tests/it/pip_install.rs @@ -486,6 +486,39 @@ fn install_requirements_txt() -> Result<()> { Ok(()) } +/// Warn (but don't fail) when unsupported flags are set in the `requirements.txt`. +#[test] +fn install_unsupported_flag() -> Result<()> { + let context = TestContext::new("3.12"); + + let requirements_txt = context.temp_dir.child("requirements.txt"); + requirements_txt.write_str(indoc! {r" + --pre + --prefer-binary :all: + iniconfig + "})?; + + uv_snapshot!(context.pip_install() + .arg("-r") + .arg("requirements.txt") + .arg("--strict"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + warning: Ignoring unsupported option in `requirements.txt`: `--pre` (hint: pass `--pre` on the command line instead) + warning: Ignoring unsupported option in `requirements.txt`: `--prefer-binary` + Resolved 1 package in [TIME] + Prepared 1 package in [TIME] + Installed 1 package in [TIME] + + iniconfig==2.0.0 + "### + ); + + Ok(()) +} + /// Install a requirements file with pins that conflict /// /// This is likely to occur in the real world when compiled on one platform then installed on another.