Skip to content

Commit 419df14

Browse files
authored
cargo-apk: Reimplement "default" (--) subcommand trailing args for cargo (#363)
* cargo-apk: Reimplement "default" subcommand trailing args for `cargo` with `--` separator `clap` [does not currently support] parsing unknown arguments into a side `Vec`, which is exactly what `cargo apk --` needs to parse a few known `cargo` arguments (such as `--target` and `-p`) but forward the rest verbatim to the underlying `cargo` subcommand. `allow_hyphen_values = true` could partially help us out with this, but it parses all remaining arguments into that `Vec` upon encountering the first unknown flag/arg, resulting in all known flags after that to also be treated as "unknown" instead of filling up our `args: Args` struct. Since [a workaround for this isn't currently functioning], introduce pure trailing args with an additional `--` separator to make it clear which arguments go to `cargo-apk` (and are almost all, except `--device`, forwarded to `cargo`) and which are only passed to `cargo <subcommand>`. [does not currently support]: clap-rs/clap#1404 [a workaround for this isn't currently functioning]: clap-rs/clap#1404 (comment) * cargo-apk: Separate unrecognized `cargo` arguments from cargo-apk `Args` With some custom logic, and assuming (validated with an `assert!`) our `Args` struct doesn't have any positionals, we can implement argument separation ourselves: this allows the user to mix and match `cargo <subcommand>` arguments with arguments recognized by `cargo-apk`, and expect `cargo-apk` to set up the environment as expected (as it did previously) by taking these arguments into account while disregarding _only_ unknown arguments. * Add `args: Args` back to `Ndk` subcommand to see them in `-h` Mixed `cargo-apk` and `cargo` args will still be split out from `cargo_args`, and they'll be appended to existing values for `args`.
1 parent f141099 commit 419df14

File tree

2 files changed

+223
-6
lines changed

2 files changed

+223
-6
lines changed

cargo-apk/src/apk.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -306,7 +306,7 @@ impl<'a> ApkBuilder<'a> {
306306
Ok(())
307307
}
308308

309-
pub fn default(&self, cargo_cmd: &str) -> Result<(), Error> {
309+
pub fn default(&self, cargo_cmd: &str, cargo_args: &[String]) -> Result<(), Error> {
310310
for target in &self.build_targets {
311311
let mut cargo = cargo_ndk(
312312
&self.ndk,
@@ -322,6 +322,10 @@ impl<'a> ApkBuilder<'a> {
322322
cargo.arg("--target").arg(triple);
323323
}
324324

325+
for additional_arg in cargo_args {
326+
cargo.arg(additional_arg);
327+
}
328+
325329
if !cargo.status()?.success() {
326330
return Err(NdkError::CmdFailed(cargo).into());
327331
}

cargo-apk/src/main.rs

Lines changed: 218 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
1+
use std::collections::HashMap;
2+
13
use cargo_apk::{ApkBuilder, Error};
24
use cargo_subcommand::Subcommand;
3-
use clap::Parser;
5+
use clap::{CommandFactory, FromArgMatches, Parser};
46

57
#[derive(Parser)]
68
struct Cmd {
@@ -17,7 +19,8 @@ enum ApkCmd {
1719
},
1820
}
1921

20-
#[derive(Parser)]
22+
#[derive(Clone, Debug, Eq, PartialEq, Parser)]
23+
#[group(skip)]
2124
struct Args {
2225
#[clap(flatten)]
2326
subcommand_args: cargo_subcommand::Args,
@@ -27,7 +30,6 @@ struct Args {
2730
}
2831

2932
#[derive(clap::Subcommand)]
30-
#[clap(trailing_var_arg = true)]
3133
enum ApkSubCmd {
3234
/// Analyze the current package and report errors, but don't build object files nor an apk
3335
#[clap(visible_alias = "c")]
@@ -44,9 +46,18 @@ enum ApkSubCmd {
4446
/// Invoke `cargo` under the detected NDK environment
4547
#[clap(name = "--")]
4648
Ndk {
49+
/// `cargo` subcommand to run
4750
cargo_cmd: String,
51+
52+
// This struct will be filled up later by arguments that are intermixed
53+
// with unknown args and ended up in `cargo_args` below.
4854
#[clap(flatten)]
4955
args: Args,
56+
57+
/// Arguments passed to cargo. Some arguments will be used to configure
58+
/// the environment similar to other `cargo apk` commands
59+
#[clap(trailing_var_arg = true, allow_hyphen_values = true)]
60+
cargo_args: Vec<String>,
5061
},
5162
/// Run a binary or example apk of the local package
5263
#[clap(visible_alias = "r")]
@@ -66,6 +77,61 @@ enum ApkSubCmd {
6677
Version,
6778
}
6879

80+
fn split_apk_and_cargo_args(mut args: Args, input: Vec<String>) -> (Args, Vec<String>) {
81+
// Clap doesn't support parsing unknown args properly:
82+
// https://github.com/clap-rs/clap/issues/1404
83+
// https://github.com/clap-rs/clap/issues/4498
84+
// Introspect the `Args` struct and extract every known arg, and whether it takes a value. Use
85+
// this information to separate out known args from unknown args, and re-parse all the known
86+
// args into an existing `args: Args` struct instance.
87+
88+
let known_args_taking_value = Args::command()
89+
.get_arguments()
90+
.flat_map(|arg| {
91+
assert!(!arg.is_positional());
92+
arg.get_short_and_visible_aliases()
93+
.iter()
94+
.flat_map(|shorts| shorts.iter().map(|short| format!("-{}", short)))
95+
.chain(
96+
arg.get_long_and_visible_aliases()
97+
.iter()
98+
.flat_map(|longs| longs.iter().map(|short| format!("--{}", short))),
99+
)
100+
.map(|arg_str| (arg_str, arg.get_action().takes_values()))
101+
// Collect to prevent lifetime issues on temporaries created above
102+
.collect::<Vec<_>>()
103+
})
104+
.collect::<HashMap<_, _>>();
105+
106+
#[derive(Debug, Default)]
107+
struct SplitArgs {
108+
apk_args: Vec<String>,
109+
cargo_args: Vec<String>,
110+
next_takes_value: bool,
111+
}
112+
113+
let split_args = input
114+
.into_iter()
115+
.fold(SplitArgs::default(), |mut split_args, elem| {
116+
let known_arg = known_args_taking_value.get(&elem);
117+
if known_arg.is_some() || split_args.next_takes_value {
118+
// Recognized arg or value for previously recognized arg
119+
split_args.apk_args.push(elem)
120+
} else {
121+
split_args.cargo_args.push(elem)
122+
}
123+
124+
split_args.next_takes_value = known_arg.copied().unwrap_or(false);
125+
split_args
126+
});
127+
128+
let m = Args::command()
129+
.no_binary_name(true)
130+
.get_matches_from(&split_args.apk_args);
131+
args.update_from_arg_matches(&m).unwrap();
132+
(args, split_args.cargo_args)
133+
}
134+
69135
fn main() -> anyhow::Result<()> {
70136
env_logger::init();
71137
let Cmd {
@@ -84,10 +150,16 @@ fn main() -> anyhow::Result<()> {
84150
builder.build(artifact)?;
85151
}
86152
}
87-
ApkSubCmd::Ndk { cargo_cmd, args } => {
153+
ApkSubCmd::Ndk {
154+
cargo_cmd,
155+
args,
156+
cargo_args,
157+
} => {
158+
let (args, cargo_args) = split_apk_and_cargo_args(args, cargo_args);
159+
88160
let cmd = Subcommand::new(args.subcommand_args)?;
89161
let builder = ApkBuilder::from_subcommand(&cmd, args.device)?;
90-
builder.default(&cargo_cmd)?;
162+
builder.default(&cargo_cmd, &cargo_args)?;
91163
}
92164
ApkSubCmd::Run { args, no_logcat } => {
93165
let cmd = Subcommand::new(args.subcommand_args)?;
@@ -107,3 +179,144 @@ fn main() -> anyhow::Result<()> {
107179
}
108180
Ok(())
109181
}
182+
183+
#[test]
184+
fn test_split_apk_and_cargo_args() {
185+
// Set up a default because cargo-subcommand doesn't derive/implement Default
186+
let args_default = Args::parse_from(std::iter::empty::<&str>());
187+
188+
assert_eq!(
189+
split_apk_and_cargo_args(args_default.clone(), vec!["--quiet".to_string()]),
190+
(
191+
Args {
192+
subcommand_args: cargo_subcommand::Args {
193+
quiet: true,
194+
..args_default.subcommand_args.clone()
195+
},
196+
..args_default.clone()
197+
},
198+
vec![]
199+
)
200+
);
201+
202+
assert_eq!(
203+
split_apk_and_cargo_args(
204+
args_default.clone(),
205+
vec!["unrecognized".to_string(), "--quiet".to_string()]
206+
),
207+
(
208+
Args {
209+
subcommand_args: cargo_subcommand::Args {
210+
quiet: true,
211+
..args_default.subcommand_args.clone()
212+
},
213+
..args_default.clone()
214+
},
215+
vec!["unrecognized".to_string()]
216+
)
217+
);
218+
219+
assert_eq!(
220+
split_apk_and_cargo_args(
221+
args_default.clone(),
222+
vec!["--unrecognized".to_string(), "--quiet".to_string()]
223+
),
224+
(
225+
Args {
226+
subcommand_args: cargo_subcommand::Args {
227+
quiet: true,
228+
..args_default.subcommand_args.clone()
229+
},
230+
..args_default.clone()
231+
},
232+
vec!["--unrecognized".to_string()]
233+
)
234+
);
235+
236+
assert_eq!(
237+
split_apk_and_cargo_args(
238+
args_default.clone(),
239+
vec!["-p".to_string(), "foo".to_string()]
240+
),
241+
(
242+
Args {
243+
subcommand_args: cargo_subcommand::Args {
244+
package: vec!["foo".to_string()],
245+
..args_default.subcommand_args.clone()
246+
},
247+
..args_default.clone()
248+
},
249+
vec![]
250+
)
251+
);
252+
253+
assert_eq!(
254+
split_apk_and_cargo_args(
255+
args_default.clone(),
256+
vec![
257+
"-p".to_string(),
258+
"foo".to_string(),
259+
"--unrecognized".to_string(),
260+
"--quiet".to_string()
261+
]
262+
),
263+
(
264+
Args {
265+
subcommand_args: cargo_subcommand::Args {
266+
quiet: true,
267+
package: vec!["foo".to_string()],
268+
..args_default.subcommand_args.clone()
269+
},
270+
..args_default.clone()
271+
},
272+
vec!["--unrecognized".to_string()]
273+
)
274+
);
275+
276+
assert_eq!(
277+
split_apk_and_cargo_args(
278+
args_default.clone(),
279+
vec![
280+
"--no-deps".to_string(),
281+
"-p".to_string(),
282+
"foo".to_string(),
283+
"--unrecognized".to_string(),
284+
"--quiet".to_string()
285+
]
286+
),
287+
(
288+
Args {
289+
subcommand_args: cargo_subcommand::Args {
290+
quiet: true,
291+
package: vec!["foo".to_string()],
292+
..args_default.subcommand_args.clone()
293+
},
294+
..args_default.clone()
295+
},
296+
vec!["--no-deps".to_string(), "--unrecognized".to_string()]
297+
)
298+
);
299+
300+
assert_eq!(
301+
split_apk_and_cargo_args(
302+
args_default.clone(),
303+
vec![
304+
"--no-deps".to_string(),
305+
"--device".to_string(),
306+
"adb:test".to_string(),
307+
"--unrecognized".to_string(),
308+
"--quiet".to_string()
309+
]
310+
),
311+
(
312+
Args {
313+
subcommand_args: cargo_subcommand::Args {
314+
quiet: true,
315+
..args_default.subcommand_args
316+
},
317+
device: Some("adb:test".to_string()),
318+
},
319+
vec!["--no-deps".to_string(), "--unrecognized".to_string()]
320+
)
321+
);
322+
}

0 commit comments

Comments
 (0)