1use crate::shell::Shell;
2use anyhow::{bail, Context as _};
3use camino::{Utf8Path, Utf8PathBuf};
4use cargo_metadata as cm;
5use easy_ext::ext;
6use indexmap::{indexset, IndexMap};
7use itertools::Itertools as _;
8use serde::{
9 de::{Deserializer, Error as _, IntoDeserializer},
10 Deserialize,
11};
12use serde_json::json;
13use std::{
14 path::{Path, PathBuf},
15 str,
16};
17use url::Url;
18
19#[derive(Deserialize, Debug, PartialEq)]
20#[serde(rename_all = "kebab-case")]
21pub(crate) struct PackageMetadataCargoCompete {
22 pub(crate) config: Option<Utf8PathBuf>,
23 #[serde(default, deserialize_with = "deserialize_bin_example")]
24 pub(crate) bin: IndexMap<String, PackageMetadataCargoCompeteBinExample>,
25 #[serde(default, deserialize_with = "deserialize_bin_example")]
26 pub(crate) example: IndexMap<String, PackageMetadataCargoCompeteBinExample>,
27}
28
29fn deserialize_bin_example<'de, D>(
30 deserializer: D,
31) -> Result<IndexMap<String, PackageMetadataCargoCompeteBinExample>, D::Error>
32where
33 D: Deserializer<'de>,
34{
35 let map = IndexMap::<String, Repr>::deserialize(deserializer)?;
36 return Ok(map
37 .into_iter()
38 .map(
39 |(
40 key,
41 Repr {
42 name,
43 alias,
44 problem,
45 },
46 )| {
47 let (name, alias) = if let Some(alias) = alias {
48 (key, alias)
49 } else if let Some(name) = name {
50 (name, key)
51 } else {
52 (key.clone(), key)
53 };
54 (
55 name,
56 PackageMetadataCargoCompeteBinExample { alias, problem },
57 )
58 },
59 )
60 .collect());
61
62 #[derive(Deserialize)]
63 #[serde(rename_all = "kebab-case")]
64 struct Repr {
65 name: Option<String>,
66 alias: Option<String>,
67 #[serde(deserialize_with = "deserialize_bin_problem")]
68 problem: Url,
69 }
70
71 fn deserialize_bin_problem<'de, D>(deserializer: D) -> Result<Url, D::Error>
72 where
73 D: Deserializer<'de>,
74 {
75 return match Repr::deserialize(deserializer) {
76 Ok(Repr::V1 { url }) | Ok(Repr::V2(url)) => Ok(url),
77 Err(_) => Err(D::Error::custom(r#"expected `"<url>" | { url: "<url>" }`"#)),
78 };
79
80 #[derive(Deserialize)]
81 #[serde(untagged)]
82 enum Repr {
83 V1 { url: Url },
84 V2(Url),
85 }
86 }
87}
88
89impl PackageMetadataCargoCompete {
90 pub(crate) fn bin_like_by_name_or_alias(
91 &self,
92 name_or_alias: impl AsRef<str>,
93 ) -> anyhow::Result<(&str, &PackageMetadataCargoCompeteBinExample)> {
94 let bin_name_or_alias = name_or_alias.as_ref();
95
96 match *itertools::chain(&self.bin, &self.example)
97 .filter(
98 |(name, PackageMetadataCargoCompeteBinExample { alias, .. })| {
99 [&**name, &**alias].contains(&bin_name_or_alias)
100 },
101 )
102 .collect::<Vec<_>>()
103 {
104 [(k, v)] => Ok((k, v)),
105 [] => bail!("no `problem` for: {}", bin_name_or_alias),
106 [..] => bail!("multiple `problem`s for {}", bin_name_or_alias),
107 }
108 }
109}
110
111#[derive(Debug, PartialEq)]
112pub(crate) struct PackageMetadataCargoCompeteBinExample {
113 pub(crate) alias: String,
114 pub(crate) problem: Url,
115}
116
117#[ext(MetadataExt)]
118impl cm::Metadata {
119 pub(crate) fn all_members(&self) -> Vec<&cm::Package> {
120 self.packages
121 .iter()
122 .filter(|cm::Package { id, .. }| self.workspace_members.contains(id))
123 .collect()
124 }
125
126 pub(crate) fn query_for_member<S: AsRef<str>>(
127 &self,
128 spec: Option<S>,
129 ) -> anyhow::Result<&cm::Package> {
130 if let Some(spec_str) = spec {
131 let spec_str = spec_str.as_ref();
132 let spec = spec_str.parse::<krates::PkgSpec>()?;
133
134 match *self
135 .packages
136 .iter()
137 .filter(|package| {
138 self.workspace_members.contains(&package.id) && spec.matches(package)
139 })
140 .collect::<Vec<_>>()
141 {
142 [] => bail!("package `{}` is not a member of the workspace", spec_str),
143 [member] => Ok(member),
144 [_, _, ..] => bail!("`{}` matched multiple members?????", spec_str),
145 }
146 } else {
147 let current_member = self
148 .resolve
149 .as_ref()
150 .and_then(|cm::Resolve { root, .. }| root.as_ref())
151 .map(|root| &self[root]);
152
153 if let Some(current_member) = current_member {
154 Ok(current_member)
155 } else {
156 match *self.workspace_members.iter().collect::<Vec<_>>() {
157 [] => bail!("this workspace has no members",),
158 [one] => Ok(&self[one]),
159 [..] => {
160 bail!(
161 "this manifest is virtual, and the workspace has {} members. specify \
162 one with `--manifest-path` or `--package`",
163 self.workspace_members.len(),
164 );
165 }
166 }
167 }
168 }
169 }
170}
171
172#[ext(PackageExt)]
173impl cm::Package {
174 pub(crate) fn manifest_dir(&self) -> &Utf8Path {
175 self.manifest_path
176 .parent()
177 .expect("`manifest_path` should end with `Cargo.toml`")
178 }
179
180 pub(crate) fn read_package_metadata(
181 &self,
182 shell: &mut Shell,
183 ) -> anyhow::Result<PackageMetadataCargoCompete> {
184 let unused = &mut indexset!();
185
186 let deserializer = self
187 .metadata
188 .get("cargo-compete")
189 .cloned()
190 .unwrap_or_else(|| json!({}))
191 .into_deserializer();
192
193 let ret = serde_ignored::deserialize(deserializer, |path| {
194 unused.insert(path.to_string());
195 })
196 .with_context(|| "could not parse `package.metadata.cargo-compete`")?;
197
198 for unused in &*unused {
199 shell.warn(format!(
200 "unused key in `package.metadata.cargo-compete`: {unused}",
201 ))?;
202 }
203
204 Ok(ret)
205 }
206
207 pub(crate) fn bin_like_target_by_name(
208 &self,
209 name: impl AsRef<str>,
210 ) -> anyhow::Result<&cm::Target> {
211 let name = name.as_ref();
212
213 self.targets
214 .iter()
215 .find(|t| {
216 t.name == name && t.kind == ["bin".to_owned()] || t.kind == ["example".to_owned()]
217 })
218 .with_context(|| format!("no bin/example target named `{}` in `{}`", name, self.name))
219 }
220
221 pub(crate) fn bin_target_by_src_path(
222 &self,
223 src_path: impl AsRef<Path>,
224 ) -> anyhow::Result<&cm::Target> {
225 let src_path = src_path.as_ref();
226
227 self.targets
228 .iter()
229 .find(|t| t.src_path == src_path && t.kind == ["bin".to_owned()])
230 .with_context(|| {
231 format!(
232 "no bin target which `src_path` is `{}` in `{}`",
233 src_path.display(),
234 self.name,
235 )
236 })
237 }
238
239 pub(crate) fn all_bin_targets_sorted(&self) -> Vec<&cm::Target> {
240 self.targets
241 .iter()
242 .filter(|cm::Target { kind, .. }| *kind == ["bin".to_owned()])
243 .sorted_by(|t1, t2| t1.name.cmp(&t2.name))
244 .collect()
245 }
246}
247
248pub(crate) fn locate_project(cwd: impl AsRef<Path>) -> anyhow::Result<PathBuf> {
249 let cwd = cwd.as_ref();
250
251 cwd.ancestors()
252 .map(|p| p.join("Cargo.toml"))
253 .find(|p| p.exists())
254 .with_context(|| {
255 format!(
256 "could not find `Cargo.toml` in `{}` or any parent directory. first, run \
257 `cargo compete init` and `cd` to a workspace",
258 cwd.display(),
259 )
260 })
261}
262
263pub(crate) fn cargo_metadata(
264 manifest_path: impl AsRef<Path>,
265 cwd: impl AsRef<Path>,
266) -> cm::Result<cm::Metadata> {
267 cm::MetadataCommand::new()
268 .manifest_path(manifest_path.as_ref())
269 .current_dir(cwd.as_ref())
270 .exec()
271}
272
273pub(crate) fn cargo_metadata_no_deps(
274 manifest_path: impl AsRef<Path>,
275 cwd: impl AsRef<Path>,
276) -> cm::Result<cm::Metadata> {
277 cm::MetadataCommand::new()
278 .manifest_path(manifest_path.as_ref())
279 .no_deps()
280 .current_dir(cwd.as_ref())
281 .exec()
282}
283
284pub(crate) fn set_cargo_config_build_target_dir(
285 dir: &Path,
286 shell: &mut Shell,
287) -> anyhow::Result<()> {
288 crate::fs::create_dir_all(dir.join(".cargo"))?;
289
290 let cargo_config_path = dir.join(".cargo").join("config.toml");
291
292 let mut cargo_config = if cargo_config_path.exists() {
293 crate::fs::read_to_string(&cargo_config_path)?
294 } else {
295 r#"[build]
296"#
297 .to_owned()
298 }
299 .parse::<toml_edit::Document>()
300 .with_context(|| {
301 format!(
302 "could not parse the TOML file at `{}`",
303 cargo_config_path.display(),
304 )
305 })?;
306
307 if cargo_config.get("build").is_none() {
308 let mut tbl = toml_edit::Table::new();
309 tbl.set_implicit(true);
310 cargo_config["build"] = toml_edit::Item::Table(tbl);
311 }
312 if { &mut cargo_config["build"]["target-dir"] }.is_none() {
313 cargo_config["build"]["target-dir"] = toml_edit::value("target");
314 crate::fs::write(&cargo_config_path, cargo_config.to_string())?;
315 shell.status("Wrote", cargo_config_path.display())?;
316 }
317 Ok(())
318}
319
320#[cfg(test)]
321mod tests {
322 use crate::project::{PackageMetadataCargoCompete, PackageMetadataCargoCompeteBinExample};
323 use indexmap::indexmap;
324 use pretty_assertions::assert_eq;
325 use toml::toml;
326
327 #[test]
328 fn deserialize_package_metadata_cargo_compete() -> anyhow::Result<()> {
329 let expected = PackageMetadataCargoCompete {
330 config: None,
331 bin: indexmap!(
332 "practice-a".to_owned() => PackageMetadataCargoCompeteBinExample {
333 alias: "a".to_owned(),
334 problem: "https://atcoder.jp/contests/practice/tasks/practice_1"
335 .parse()
336 .unwrap(),
337 },
338 "practice-b".to_owned() => PackageMetadataCargoCompeteBinExample {
339 alias: "b".to_owned(),
340 problem: "https://atcoder.jp/contests/practice/tasks/practice_2"
341 .parse()
342 .unwrap(),
343 },
344 ),
345 example: indexmap!(),
346 };
347
348 assert_eq!(
349 expected,
350 toml! {
351 [bin]
352 practice-a = { alias = "a", problem = "https://atcoder.jp/contests/practice/tasks/practice_1" }
353 practice-b = { alias = "b", problem = "https://atcoder.jp/contests/practice/tasks/practice_2" }
354 }
355 .try_into::<PackageMetadataCargoCompete>()?,
356 );
357
358 let expected = PackageMetadataCargoCompete {
359 config: None,
360 bin: indexmap!(
361 "aplusb".to_owned() => PackageMetadataCargoCompeteBinExample {
362 alias: "aplusb".to_owned(),
363 problem: "https://judge.yosupo.jp/problem/aplusb".parse().unwrap(),
364 },
365 ),
366 example: indexmap!(),
367 };
368
369 assert_eq!(
370 expected,
371 toml! {
372 [bin]
373 aplusb = { problem = "https://judge.yosupo.jp/problem/aplusb" }
374 }
375 .try_into::<PackageMetadataCargoCompete>()?,
376 );
377
378 let expected = PackageMetadataCargoCompete {
379 config: None,
380 bin: indexmap!(),
381 example: indexmap!(
382 "aplusb".to_owned() => PackageMetadataCargoCompeteBinExample {
383 alias: "aplusb".to_owned(),
384 problem: "https://judge.yosupo.jp/problem/aplusb".parse().unwrap(),
385 },
386 ),
387 };
388
389 assert_eq!(
390 expected,
391 toml! {
392 [example]
393 aplusb = { problem = "https://judge.yosupo.jp/problem/aplusb" }
394 }
395 .try_into::<PackageMetadataCargoCompete>()?,
396 );
397
398 Ok(())
399 }
400}