cargo_compete/
project.rs

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}