From 78edefea9d8e4820d7ce7c1d846f89e60fe3b81c Mon Sep 17 00:00:00 2001 From: Zalathar Date: Mon, 11 Nov 2024 21:42:42 +1100 Subject: [PATCH] Overhaul the `-l` option parser (for linking to native libs) --- compiler/rustc_session/src/config.rs | 11 +- .../rustc_session/src/config/native_libs.rs | 284 +++++++++++------- .../src/config/native_libs/tests.rs | 50 +++ ...ve_link_modifiers_as_needed.in_flag.stderr | 2 +- 4 files changed, 225 insertions(+), 122 deletions(-) create mode 100644 compiler/rustc_session/src/config/native_libs/tests.rs diff --git a/compiler/rustc_session/src/config.rs b/compiler/rustc_session/src/config.rs index 61859466a2c8d..f6e6fd33c48ea 100644 --- a/compiler/rustc_session/src/config.rs +++ b/compiler/rustc_session/src/config.rs @@ -31,7 +31,7 @@ use rustc_target::spec::{ use tracing::debug; pub use crate::config::cfg::{Cfg, CheckCfg, ExpectedValues}; -use crate::config::native_libs::parse_libs; +use crate::config::native_libs::parse_native_libs; use crate::errors::FileWriteFail; pub use crate::options::*; use crate::search_paths::SearchPath; @@ -2508,7 +2508,10 @@ pub fn build_session_options(early_dcx: &mut EarlyDiagCtxt, matches: &getopts::M let debuginfo = select_debuginfo(matches, &cg); let debuginfo_compression = unstable_opts.debuginfo_compression; - let libs = parse_libs(early_dcx, matches); + let crate_name = matches.opt_str("crate-name"); + let unstable_features = UnstableFeatures::from_environment(crate_name.as_deref()); + // Parse any `-l` flags, which link to native libraries. + let libs = parse_native_libs(early_dcx, &unstable_opts, unstable_features, matches); let test = matches.opt_present("test"); @@ -2523,8 +2526,6 @@ pub fn build_session_options(early_dcx: &mut EarlyDiagCtxt, matches: &getopts::M let externs = parse_externs(early_dcx, matches, &unstable_opts); - let crate_name = matches.opt_str("crate-name"); - let remap_path_prefix = parse_remap_path_prefix(early_dcx, matches, &unstable_opts); let pretty = parse_pretty(early_dcx, &unstable_opts); @@ -2598,7 +2599,7 @@ pub fn build_session_options(early_dcx: &mut EarlyDiagCtxt, matches: &getopts::M error_format, diagnostic_width, externs, - unstable_features: UnstableFeatures::from_environment(crate_name.as_deref()), + unstable_features, crate_name, libs, debug_assertions, diff --git a/compiler/rustc_session/src/config/native_libs.rs b/compiler/rustc_session/src/config/native_libs.rs index 288756925f27e..f1f0aeb5e599b 100644 --- a/compiler/rustc_session/src/config/native_libs.rs +++ b/compiler/rustc_session/src/config/native_libs.rs @@ -1,140 +1,192 @@ +//! Parser for the `-l` command-line option, which links the generated crate to +//! a native library. +//! +//! (There is also a similar but separate syntax for `#[link]` attributes, +//! which have their own parser in `rustc_metadata`.) + +use rustc_feature::UnstableFeatures; + use crate::EarlyDiagCtxt; -use crate::config::nightly_options; +use crate::config::UnstableOptions; use crate::utils::{NativeLib, NativeLibKind}; -pub(crate) fn parse_libs(early_dcx: &EarlyDiagCtxt, matches: &getopts::Matches) -> Vec { - matches - .opt_strs("l") - .into_iter() - .map(|s| { - // Parse string of the form "[KIND[:MODIFIERS]=]lib[:new_name]", - // where KIND is one of "dylib", "framework", "static", "link-arg" and - // where MODIFIERS are a comma separated list of supported modifiers - // (bundle, verbatim, whole-archive, as-needed). Each modifier is prefixed - // with either + or - to indicate whether it is enabled or disabled. - // The last value specified for a given modifier wins. - let (name, kind, verbatim) = match s.split_once('=') { - None => (s, NativeLibKind::Unspecified, None), - Some((kind, name)) => { - let (kind, verbatim) = parse_native_lib_kind(early_dcx, matches, kind); - (name.to_string(), kind, verbatim) - } - }; - - let (name, new_name) = match name.split_once(':') { - None => (name, None), - Some((name, new_name)) => (name.to_string(), Some(new_name.to_owned())), - }; - if name.is_empty() { - early_dcx.early_fatal("library name must not be empty"); - } - NativeLib { name, new_name, kind, verbatim } - }) - .collect() -} +#[cfg(test)] +mod tests; -fn parse_native_lib_kind( +/// Parses all `-l` options. +pub(crate) fn parse_native_libs( early_dcx: &EarlyDiagCtxt, + unstable_opts: &UnstableOptions, + unstable_features: UnstableFeatures, matches: &getopts::Matches, - kind: &str, -) -> (NativeLibKind, Option) { - let (kind, modifiers) = match kind.split_once(':') { - None => (kind, None), - Some((kind, modifiers)) => (kind, Some(modifiers)), +) -> Vec { + let cx = ParseNativeLibCx { + early_dcx, + unstable_options_enabled: unstable_opts.unstable_options, + is_nightly: unstable_features.is_nightly_build(), }; + matches.opt_strs("l").into_iter().map(|value| parse_native_lib(&cx, &value)).collect() +} + +struct ParseNativeLibCx<'a> { + early_dcx: &'a EarlyDiagCtxt, + unstable_options_enabled: bool, + is_nightly: bool, +} + +impl ParseNativeLibCx<'_> { + /// If unstable values are not permitted, exits with a fatal error made by + /// combining the given strings. + fn on_unstable_value(&self, message: &str, if_nightly: &str, if_stable: &str) { + if self.unstable_options_enabled { + return; + } + + let suffix = if self.is_nightly { if_nightly } else { if_stable }; + self.early_dcx.early_fatal(format!("{message}{suffix}")); + } +} - let kind = match kind { +/// Parses the value of a single `-l` option. +fn parse_native_lib(cx: &ParseNativeLibCx<'_>, value: &str) -> NativeLib { + let NativeLibParts { kind, modifiers, name, new_name } = split_native_lib_value(value); + + let kind = kind.map_or(NativeLibKind::Unspecified, |kind| match kind { "static" => NativeLibKind::Static { bundle: None, whole_archive: None }, "dylib" => NativeLibKind::Dylib { as_needed: None }, "framework" => NativeLibKind::Framework { as_needed: None }, "link-arg" => { - if !nightly_options::is_unstable_enabled(matches) { - let why = if nightly_options::match_is_nightly_build(matches) { - " and only accepted on the nightly compiler" - } else { - ", the `-Z unstable-options` flag must also be passed to use it" - }; - early_dcx.early_fatal(format!("library kind `link-arg` is unstable{why}")) - } + cx.on_unstable_value( + "library kind `link-arg` is unstable", + ", the `-Z unstable-options` flag must also be passed to use it", + " and only accepted on the nightly compiler", + ); NativeLibKind::LinkArg } - _ => early_dcx.early_fatal(format!( + _ => cx.early_dcx.early_fatal(format!( "unknown library kind `{kind}`, expected one of: static, dylib, framework, link-arg" )), + }); + + // Provisionally create the result, so that modifiers can modify it. + let mut native_lib = NativeLib { + name: name.to_owned(), + new_name: new_name.map(str::to_owned), + kind, + verbatim: None, }; - match modifiers { - None => (kind, None), - Some(modifiers) => parse_native_lib_modifiers(early_dcx, kind, modifiers, matches), + + if let Some(modifiers) = modifiers { + // If multiple modifiers are present, they are separated by commas. + for modifier in modifiers.split(',') { + parse_and_apply_modifier(cx, modifier, &mut native_lib); + } } + + if native_lib.name.is_empty() { + cx.early_dcx.early_fatal("library name must not be empty"); + } + + native_lib } -fn parse_native_lib_modifiers( - early_dcx: &EarlyDiagCtxt, - mut kind: NativeLibKind, - modifiers: &str, - matches: &getopts::Matches, -) -> (NativeLibKind, Option) { - let mut verbatim = None; - for modifier in modifiers.split(',') { - let (modifier, value) = match modifier.strip_prefix(['+', '-']) { - Some(m) => (m, modifier.starts_with('+')), - None => early_dcx.early_fatal( - "invalid linking modifier syntax, expected '+' or '-' prefix \ - before one of: bundle, verbatim, whole-archive, as-needed", - ), - }; - - let report_unstable_modifier = || { - if !nightly_options::is_unstable_enabled(matches) { - let why = if nightly_options::match_is_nightly_build(matches) { - " and only accepted on the nightly compiler" - } else { - ", the `-Z unstable-options` flag must also be passed to use it" - }; - early_dcx.early_fatal(format!("linking modifier `{modifier}` is unstable{why}")) - } - }; - let assign_modifier = |dst: &mut Option| { - if dst.is_some() { - let msg = format!("multiple `{modifier}` modifiers in a single `-l` option"); - early_dcx.early_fatal(msg) - } else { - *dst = Some(value); - } - }; - match (modifier, &mut kind) { - ("bundle", NativeLibKind::Static { bundle, .. }) => assign_modifier(bundle), - ("bundle", _) => early_dcx.early_fatal( - "linking modifier `bundle` is only compatible with `static` linking kind", - ), - - ("verbatim", _) => assign_modifier(&mut verbatim), - - ("whole-archive", NativeLibKind::Static { whole_archive, .. }) => { - assign_modifier(whole_archive) - } - ("whole-archive", _) => early_dcx.early_fatal( - "linking modifier `whole-archive` is only compatible with `static` linking kind", - ), - - ("as-needed", NativeLibKind::Dylib { as_needed }) - | ("as-needed", NativeLibKind::Framework { as_needed }) => { - report_unstable_modifier(); - assign_modifier(as_needed) - } - ("as-needed", _) => early_dcx.early_fatal( - "linking modifier `as-needed` is only compatible with \ - `dylib` and `framework` linking kinds", - ), - - // Note: this error also excludes the case with empty modifier - // string, like `modifiers = ""`. - _ => early_dcx.early_fatal(format!( - "unknown linking modifier `{modifier}`, expected one \ - of: bundle, verbatim, whole-archive, as-needed" - )), +/// Parses one of the comma-separated modifiers (prefixed by `+` or `-`), and +/// modifies `native_lib` appropriately. +/// +/// Exits with a fatal error if a malformed/unknown/inappropriate modifier is +/// found. +fn parse_and_apply_modifier(cx: &ParseNativeLibCx<'_>, modifier: &str, native_lib: &mut NativeLib) { + let early_dcx = cx.early_dcx; + + // Split off the leading `+` or `-` into a boolean value. + let (modifier, value) = match modifier.split_at_checked(1) { + Some(("+", m)) => (m, true), + Some(("-", m)) => (m, false), + _ => cx.early_dcx.early_fatal( + "invalid linking modifier syntax, expected '+' or '-' prefix \ + before one of: bundle, verbatim, whole-archive, as-needed", + ), + }; + + // Assigns the value (from `+` or `-`) to an empty `Option`, or emits + // a fatal error if the option has already been set. + let assign_modifier = |opt_bool: &mut Option| { + if opt_bool.is_some() { + let msg = format!("multiple `{modifier}` modifiers in a single `-l` option"); + early_dcx.early_fatal(msg) } + *opt_bool = Some(value); + }; + + // Check that the modifier is applicable to the native lib kind, and apply it. + match (modifier, &mut native_lib.kind) { + ("bundle", NativeLibKind::Static { bundle, .. }) => assign_modifier(bundle), + ("bundle", _) => early_dcx + .early_fatal("linking modifier `bundle` is only compatible with `static` linking kind"), + + ("verbatim", _) => assign_modifier(&mut native_lib.verbatim), + + ("whole-archive", NativeLibKind::Static { whole_archive, .. }) => { + assign_modifier(whole_archive) + } + ("whole-archive", _) => early_dcx.early_fatal( + "linking modifier `whole-archive` is only compatible with `static` linking kind", + ), + + ("as-needed", NativeLibKind::Dylib { as_needed }) + | ("as-needed", NativeLibKind::Framework { as_needed }) => { + cx.on_unstable_value( + "linking modifier `as-needed` is unstable", + ", the `-Z unstable-options` flag must also be passed to use it", + " and only accepted on the nightly compiler", + ); + assign_modifier(as_needed) + } + ("as-needed", _) => early_dcx.early_fatal( + "linking modifier `as-needed` is only compatible with \ + `dylib` and `framework` linking kinds", + ), + + _ => early_dcx.early_fatal(format!( + "unknown linking modifier `{modifier}`, expected one \ + of: bundle, verbatim, whole-archive, as-needed" + )), } +} + +#[derive(Debug, PartialEq, Eq)] +struct NativeLibParts<'a> { + kind: Option<&'a str>, + modifiers: Option<&'a str>, + name: &'a str, + new_name: Option<&'a str>, +} + +/// Splits a string of the form `[KIND[:MODIFIERS]=]NAME[:NEW_NAME]` into those +/// individual parts. This cannot fail, but the resulting strings require +/// further validation. +fn split_native_lib_value(value: &str) -> NativeLibParts<'_> { + // Split the initial value into `[KIND=]NAME`. + let name = value; + let (kind, name) = match name.split_once('=') { + Some((prefix, name)) => (Some(prefix), name), + None => (None, name), + }; + + // Split the kind part, if present, into `KIND[:MODIFIERS]`. + let (kind, modifiers) = match kind { + Some(kind) => match kind.split_once(':') { + Some((kind, modifiers)) => (Some(kind), Some(modifiers)), + None => (Some(kind), None), + }, + None => (None, None), + }; + + // Split the name part into `NAME[:NEW_NAME]`. + let (name, new_name) = match name.split_once(':') { + Some((name, new_name)) => (name, Some(new_name)), + None => (name, None), + }; - (kind, verbatim) + NativeLibParts { kind, modifiers, name, new_name } } diff --git a/compiler/rustc_session/src/config/native_libs/tests.rs b/compiler/rustc_session/src/config/native_libs/tests.rs new file mode 100644 index 0000000000000..3bcab93ef4b8f --- /dev/null +++ b/compiler/rustc_session/src/config/native_libs/tests.rs @@ -0,0 +1,50 @@ +use crate::config::native_libs::{NativeLibParts, split_native_lib_value}; + +#[test] +fn split() { + // This is a unit test for some implementation details, so consider deleting + // it if it gets in the way. + use NativeLibParts as P; + + let examples = &[ + ("", P { kind: None, modifiers: None, name: "", new_name: None }), + ("foo", P { kind: None, modifiers: None, name: "foo", new_name: None }), + ("foo:", P { kind: None, modifiers: None, name: "foo", new_name: Some("") }), + ("foo:bar", P { kind: None, modifiers: None, name: "foo", new_name: Some("bar") }), + (":bar", P { kind: None, modifiers: None, name: "", new_name: Some("bar") }), + ("kind=foo", P { kind: Some("kind"), modifiers: None, name: "foo", new_name: None }), + (":mods=foo", P { kind: Some(""), modifiers: Some("mods"), name: "foo", new_name: None }), + (":mods=:bar", P { + kind: Some(""), + modifiers: Some("mods"), + name: "", + new_name: Some("bar"), + }), + ("kind=foo:bar", P { + kind: Some("kind"), + modifiers: None, + name: "foo", + new_name: Some("bar"), + }), + ("kind:mods=foo", P { + kind: Some("kind"), + modifiers: Some("mods"), + name: "foo", + new_name: None, + }), + ("kind:mods=foo:bar", P { + kind: Some("kind"), + modifiers: Some("mods"), + name: "foo", + new_name: Some("bar"), + }), + ("::==::", P { kind: Some(""), modifiers: Some(":"), name: "=", new_name: Some(":") }), + ("==::==", P { kind: Some(""), modifiers: None, name: "=", new_name: Some(":==") }), + ]; + + for &(value, ref expected) in examples { + println!("{value:?}"); + let actual = split_native_lib_value(value); + assert_eq!(&actual, expected); + } +} diff --git a/tests/ui/feature-gates/feature-gate-native_link_modifiers_as_needed.in_flag.stderr b/tests/ui/feature-gates/feature-gate-native_link_modifiers_as_needed.in_flag.stderr index 1f0b9d04ef676..8f74e9d6f169c 100644 --- a/tests/ui/feature-gates/feature-gate-native_link_modifiers_as_needed.in_flag.stderr +++ b/tests/ui/feature-gates/feature-gate-native_link_modifiers_as_needed.in_flag.stderr @@ -1,2 +1,2 @@ -error: linking modifier `as-needed` is unstable and only accepted on the nightly compiler +error: linking modifier `as-needed` is unstable, the `-Z unstable-options` flag must also be passed to use it