cargo_gazelle/
targets.rs

1// Copyright Materialize, Inc. and contributors. All rights reserved.
2//
3// Use of this software is governed by the Business Source License
4// included in the LICENSE file.
5//
6// As of the Change Date specified in that file, in accordance with
7// the Business Source License, use of this software will be governed
8// by the Apache License, Version 2.0.
9
10//! Definitions for the "rules_rust" Bazel targets.
11
12use convert_case::{Case, Casing};
13use guppy::DependencyKind;
14use guppy::graph::feature::{FeatureLabel, FeatureSet, StandardFeatures};
15use guppy::graph::{
16    BuildTarget, BuildTargetId, BuildTargetKind, DependencyDirection, PackageMetadata,
17};
18use guppy::platform::EnabledTernary;
19
20use std::collections::{BTreeMap, BTreeSet};
21use std::fmt::{self, Write};
22use std::str::FromStr;
23
24use crate::config::{CrateConfig, GlobalConfig};
25use crate::context::CrateContext;
26use crate::platforms::PlatformVariant;
27use crate::rules::Rule;
28use crate::{Alias, Dict, Field, FileGroup, Glob, List, QuotedString, Select};
29
30use super::{AutoIndentingWriter, ToBazelDefinition};
31
32/// Name given to the Bazel [`filegroup`](https://bazel.build/reference/be/general#filegroup) that
33/// exports all protobuf files.
34const PROTO_FILEGROUP_NAME: &str = "all_protos";
35
36pub trait RustTarget: ToBazelDefinition {
37    /// Returns the Bazel rules that need to be loaded for this target.
38    fn rules(&self) -> Vec<Rule>;
39}
40
41impl<T: RustTarget> RustTarget for Option<T> {
42    fn rules(&self) -> Vec<Rule> {
43        match self {
44            Some(t) => t.rules(),
45            None => vec![],
46        }
47    }
48}
49
50/// [`rust_library`](https://bazelbuild.github.io/rules_rust/defs.html#rust_library)
51#[derive(Debug)]
52pub struct RustLibrary {
53    name: Field<QuotedString>,
54    version: Field<QuotedString>,
55    is_proc_macro: bool,
56    features: Field<List<QuotedString>>,
57    aliases: Field<Aliases>,
58    lint_config: Field<QuotedString>,
59    deps: Field<List<QuotedString>>,
60    proc_macro_deps: Field<List<QuotedString>>,
61    data: Field<List<QuotedString>>,
62    compile_data: Field<List<QuotedString>>,
63    disable_pipelining: Option<Field<bool>>,
64    rustc_flags: Field<List<QuotedString>>,
65    rustc_env: Field<Dict<QuotedString, QuotedString>>,
66    /// Other targets, e.g. unit tests, that we generate for a library.
67    extra_targets: Vec<Box<dyn ToBazelDefinition>>,
68}
69
70impl RustTarget for RustLibrary {
71    fn rules(&self) -> Vec<Rule> {
72        let primary_rule = if self.is_proc_macro {
73            Rule::RustProcMacro
74        } else {
75            Rule::RustLibrary
76        };
77
78        vec![primary_rule, Rule::RustTest, Rule::RustDocTest]
79    }
80}
81
82impl RustLibrary {
83    pub fn generate(
84        config: &GlobalConfig,
85        metadata: &PackageMetadata,
86        crate_config: &CrateConfig,
87        build_script: Option<&CargoBuildScript>,
88    ) -> Result<Option<Self>, anyhow::Error> {
89        if crate_config.lib().common().skip() {
90            return Ok(None);
91        }
92
93        // Not all crates have a `lib.rs` or require a rust_library target.
94        let library_target = metadata
95            .build_targets()
96            .find(|target| matches!(target.id(), BuildTargetId::Library));
97        if library_target.is_none() {
98            let name = metadata.name();
99            tracing::debug!("no library target found for {name}, skipping rust_library",);
100            return Ok(None);
101        }
102
103        let name = metadata.name().to_case(Case::Snake);
104        let name = QuotedString::new(name);
105
106        // Collect all of the crate features.
107        //
108        // Note: Cargo features and Bazel don't work together very well, so by
109        // default we just enable all features.
110        let features: List<_> = if let Some(x) = crate_config.lib().features_override() {
111            x.into_iter().map(QuotedString::from).collect()
112        } else {
113            let (common, extras) = crate_features(config, metadata)?;
114            let mut features = List::new(common);
115
116            if !extras.is_empty() {
117                let select: Select<List<QuotedString>> = Select::new(extras, vec![]);
118                features = features.concat_other(select);
119            }
120
121            features
122        };
123
124        // Collect all dependencies.
125        let all_deps = WorkspaceDependencies::new(config, metadata);
126        let (deps, extra_deps) = all_deps.iter(DependencyKind::Normal, false);
127        let mut deps = List::new(deps).concat_other(AllCrateDeps::default().normal());
128
129        // Add the build script as a dependency, if we have one.
130        if let Some(build_script) = build_script {
131            let build_script_target = format!(":{}", build_script.name.value.unquoted());
132            deps.push_front(QuotedString::new(build_script_target));
133        }
134        // Add extra platform deps if there are any.
135        if !extra_deps.is_empty() {
136            let select: Select<List<QuotedString>> = Select::new(extra_deps, vec![]);
137            deps = deps.concat_other(select);
138        }
139
140        // Collect all proc macro dependencies.
141        let (proc_macro_deps, extra_proc_macro_deps) = all_deps.iter(DependencyKind::Normal, true);
142        let mut proc_macro_deps =
143            List::new(proc_macro_deps).concat_other(AllCrateDeps::default().proc_macro());
144
145        // Add extra platform deps if there are any.
146        if !extra_proc_macro_deps.is_empty() {
147            let select: Select<List<QuotedString>> = Select::new(extra_proc_macro_deps, vec![]);
148            proc_macro_deps = proc_macro_deps.concat_other(select);
149        }
150
151        // TODO(parkmycar): Make the lint_config configurable.
152        let lint_config = QuotedString::new(":lints");
153
154        // For every library we also generate the tests targets.
155        let unit_test = RustTest::library(config, metadata, crate_config, features.clone())?;
156        let doc_tests = RustDocTest::generate(config, metadata, crate_config)?;
157        let mut extra_targets: Vec<Box<dyn ToBazelDefinition>> =
158            vec![Box::new(unit_test), Box::new(doc_tests)];
159
160        // Generate an alias with the same name as the containing directory that points to the
161        // library target. This allows you to build the library with a short-hand notation, e.g.
162        // `//src/compute-types` instead of `//src/compute-types:mz_compute_types`.
163        let crate_filename = metadata
164            .manifest_path()
165            .parent()
166            .and_then(|path| path.file_name());
167        if let Some(crate_filename) = crate_filename {
168            let other_target_conflicts = metadata
169                .build_targets()
170                .map(|target| target.name())
171                .any(|target_name| target_name.to_case(Case::Snake) == crate_filename);
172            if !other_target_conflicts {
173                let alias = Alias::new(crate_filename, name.unquoted());
174                extra_targets.insert(0, Box::new(alias));
175            }
176        }
177
178        // Extend with any extra config specified in the Cargo.toml.
179        let lib_common = crate_config.lib().common();
180
181        deps.extend(crate_config.lib().extra_deps());
182        proc_macro_deps.extend(crate_config.lib().extra_proc_macro_deps());
183
184        let (paths, globs) = lib_common.data();
185        let mut data = List::new(paths);
186        if let Some(globs) = globs {
187            data = data.concat_other(Glob::new(globs));
188        }
189
190        let (paths, globs) = lib_common.compile_data();
191        let mut compile_data = List::new(paths);
192        if let Some(globs) = globs {
193            compile_data = compile_data.concat_other(Glob::new(globs));
194        }
195
196        let rustc_flags = List::new(lib_common.rustc_flags());
197        let rustc_env = Dict::new(lib_common.rustc_env());
198
199        let disable_pipelining = if let Some(flag) = crate_config.lib().disable_pipelining() {
200            Some(flag)
201        } else {
202            // If a library target contains compile data then we disable pipelining because it
203            // messes with the crate hash and leads to hard to debug build errors.
204            (!compile_data.is_empty()).then_some(true)
205        };
206
207        Ok(Some(RustLibrary {
208            name: Field::new("name", name),
209            version: Field::new("version", metadata.version().to_string().into()),
210            is_proc_macro: metadata.is_proc_macro(),
211            features: Field::new("crate_features", features),
212            aliases: Field::new("aliases", Aliases::default().normal().proc_macro()),
213            lint_config: Field::new("lint_config", lint_config),
214            deps: Field::new("deps", deps),
215            proc_macro_deps: Field::new("proc_macro_deps", proc_macro_deps),
216            data: Field::new("data", data),
217            compile_data: Field::new("compile_data", compile_data),
218            disable_pipelining: disable_pipelining.map(|v| Field::new("disable_pipelining", v)),
219            rustc_flags: Field::new("rustc_flags", rustc_flags),
220            rustc_env: Field::new("rustc_env", rustc_env),
221            extra_targets,
222        }))
223    }
224}
225
226impl ToBazelDefinition for RustLibrary {
227    fn format(&self, w: &mut dyn fmt::Write) -> Result<(), fmt::Error> {
228        let mut w = AutoIndentingWriter::new(w);
229
230        let kind = if self.is_proc_macro {
231            "rust_proc_macro"
232        } else {
233            "rust_library"
234        };
235
236        writeln!(w, "{kind}(")?;
237        {
238            let mut w = w.indent();
239
240            self.name.format(&mut w)?;
241            self.version.format(&mut w)?;
242
243            writeln!(w, r#"srcs = glob(["src/**/*.rs"]),"#)?;
244
245            self.features.format(&mut w)?;
246            self.aliases.format(&mut w)?;
247            self.lint_config.format(&mut w)?;
248            self.deps.format(&mut w)?;
249            self.proc_macro_deps.format(&mut w)?;
250            self.data.format(&mut w)?;
251            self.compile_data.format(&mut w)?;
252            self.disable_pipelining.format(&mut w)?;
253            self.rustc_flags.format(&mut w)?;
254            self.rustc_env.format(&mut w)?;
255        }
256        writeln!(w, ")")?;
257
258        for extra_target in &self.extra_targets {
259            writeln!(w)?;
260            extra_target.format(&mut w)?;
261        }
262
263        Ok(())
264    }
265}
266
267/// [`rust_binary`](https://bazelbuild.github.io/rules_rust/defs.html#rust_binary)
268#[derive(Debug)]
269pub struct RustBinary {
270    name: Field<QuotedString>,
271    version: Field<QuotedString>,
272    crate_root: Field<QuotedString>,
273    features: Field<List<QuotedString>>,
274    lint_config: Field<QuotedString>,
275    aliases: Field<Aliases>,
276    deps: Field<List<QuotedString>>,
277    proc_macro_deps: Field<List<QuotedString>>,
278    data: Field<List<QuotedString>>,
279    compile_data: Field<List<QuotedString>>,
280    env: Field<Dict<QuotedString, QuotedString>>,
281    rustc_flags: Field<List<QuotedString>>,
282    rustc_env: Field<Dict<QuotedString, QuotedString>>,
283}
284
285impl RustTarget for RustBinary {
286    fn rules(&self) -> Vec<Rule> {
287        vec![Rule::RustBinary]
288    }
289}
290
291impl RustBinary {
292    pub fn generate(
293        config: &GlobalConfig,
294        metadata: &PackageMetadata,
295        crate_config: &CrateConfig,
296        target: &BuildTarget,
297    ) -> Result<Option<Self>, anyhow::Error> {
298        let crate_root_path = metadata
299            .manifest_path()
300            .parent()
301            .ok_or_else(|| anyhow::anyhow!("crate is at the root of the filesystem?"))?;
302        let name = match target.id() {
303            BuildTargetId::Binary(name) => name,
304            x => panic!(
305                "can only generate `rust_binary` rules for binary build targets, found {x:?}"
306            ),
307        };
308        let name = name.to_case(Case::Snake);
309
310        let maybe_library = metadata
311            .build_targets()
312            .find(|target| matches!(target.id(), BuildTargetId::Library));
313
314        // Adjust the target name to avoid a possible conflict.
315        tracing::debug!(
316            maybe_library = ?maybe_library.as_ref().map(|t| t.name()),
317            binary_name = name,
318            "name for rust_binary"
319        );
320        let target_name = match &maybe_library {
321            Some(library) if library.name() == name => QuotedString::new(format!("{}_bin", name)),
322            _ => QuotedString::new(&name),
323        };
324
325        if crate_config.binary(&name).common().skip() {
326            return Ok(None);
327        }
328
329        let binary_path = target.path();
330        let binary_path = binary_path
331            .strip_prefix(crate_root_path)
332            .map_err(|_| anyhow::anyhow!("binary is not inside workspace?"))?;
333        let binary_path = QuotedString::new(binary_path.to_string());
334
335        // Collect all dependencies.
336        let all_deps = WorkspaceDependencies::new(config, metadata);
337        let (deps, extra_deps) = all_deps.iter(DependencyKind::Normal, false);
338        let (proc_macro_deps, extra_proc_macro_deps) = all_deps.iter(DependencyKind::Normal, true);
339
340        let mut deps: List<QuotedString> =
341            List::new(deps).concat_other(AllCrateDeps::default().normal());
342        // Add extra platform deps if there are any.
343        if !extra_deps.is_empty() {
344            let select: Select<List<QuotedString>> = Select::new(extra_deps, vec![]);
345            deps = deps.concat_other(select);
346        }
347
348        let mut proc_macro_deps: List<QuotedString> =
349            List::new(proc_macro_deps).concat_other(AllCrateDeps::default().proc_macro());
350        // Add extra platform deps if there are any.
351        if !extra_proc_macro_deps.is_empty() {
352            let select: Select<List<QuotedString>> = Select::new(extra_proc_macro_deps, vec![]);
353            proc_macro_deps = proc_macro_deps.concat_other(select);
354        }
355
356        // TODO(parkmycar): Make the lint_config configurable.
357        let lint_config = QuotedString::new(":lints");
358
359        // Add the library crate as a dep if it isn't already.
360        if maybe_library.is_some() {
361            let dep = format!(":{}", metadata.name().to_case(Case::Snake));
362            if metadata.is_proc_macro() {
363                if !proc_macro_deps.iter().any(|d| d.unquoted().ends_with(&dep)) {
364                    proc_macro_deps.push_front(dep);
365                }
366            } else {
367                if !deps.iter().any(|d| d.unquoted().ends_with(&dep)) {
368                    deps.push_front(dep);
369                }
370            }
371        }
372
373        // Extend with any extra config specified in the Cargo.toml.
374        let bin_config = crate_config.binary(&name);
375
376        deps.extend(crate_config.lib().extra_deps());
377        proc_macro_deps.extend(crate_config.lib().extra_proc_macro_deps());
378
379        let (paths, globs) = bin_config.common().data();
380        let data = List::new(paths).concat_other(globs.map(Glob::new));
381
382        let (paths, globs) = bin_config.common().compile_data();
383        let compile_data = List::new(paths).concat_other(globs.map(Glob::new));
384
385        let env = Dict::new(bin_config.env());
386        let rustc_flags = List::new(bin_config.common().rustc_flags());
387        let rustc_env = Dict::new(bin_config.common().rustc_env());
388
389        Ok(Some(RustBinary {
390            name: Field::new("name", target_name),
391            version: Field::new("version", metadata.version().to_string().into()),
392            crate_root: Field::new("crate_root", binary_path),
393            features: Field::new("features", List::empty()),
394            aliases: Field::new("aliases", Aliases::default().normal().proc_macro()),
395            lint_config: Field::new("lint_config", lint_config),
396            deps: Field::new("deps", deps),
397            proc_macro_deps: Field::new("proc_macro_deps", proc_macro_deps),
398            data: Field::new("data", data),
399            compile_data: Field::new("compile_data", compile_data),
400            env: Field::new("env", env),
401            rustc_flags: Field::new("rustc_flags", rustc_flags),
402            rustc_env: Field::new("rustc_env", rustc_env),
403        }))
404    }
405}
406
407impl ToBazelDefinition for RustBinary {
408    fn format(&self, w: &mut dyn fmt::Write) -> Result<(), fmt::Error> {
409        let mut w = AutoIndentingWriter::new(w);
410
411        writeln!(w, "rust_binary(")?;
412        {
413            let mut w = w.indent();
414
415            self.name.format(&mut w)?;
416            self.version.format(&mut w)?;
417            self.crate_root.format(&mut w)?;
418
419            writeln!(w, r#"srcs = glob(["src/**/*.rs"]),"#)?;
420
421            self.features.format(&mut w)?;
422            self.aliases.format(&mut w)?;
423            self.lint_config.format(&mut w)?;
424            self.deps.format(&mut w)?;
425            self.proc_macro_deps.format(&mut w)?;
426            self.compile_data.format(&mut w)?;
427            self.data.format(&mut w)?;
428            self.env.format(&mut w)?;
429            self.rustc_flags.format(&mut w)?;
430            self.rustc_env.format(&mut w)?;
431        }
432        writeln!(w, ")")?;
433
434        Ok(())
435    }
436}
437
438/// [`rust_test`](https://bazelbuild.github.io/rules_rust/defs.html#rust_test)
439#[derive(Debug)]
440pub struct RustTest {
441    name: Field<QuotedString>,
442    version: Field<QuotedString>,
443    kind: RustTestKind,
444    features: Field<List<QuotedString>>,
445    aliases: Field<Aliases>,
446    lint_config: Field<QuotedString>,
447    deps: Field<List<QuotedString>>,
448    proc_macro_deps: Field<List<QuotedString>>,
449    size: Field<RustTestSize>,
450    data: Field<List<QuotedString>>,
451    compile_data: Field<List<QuotedString>>,
452    env: Field<Dict<QuotedString, QuotedString>>,
453    rustc_flags: Field<List<QuotedString>>,
454    rustc_env: Field<Dict<QuotedString, QuotedString>>,
455}
456
457impl RustTarget for RustTest {
458    fn rules(&self) -> Vec<Rule> {
459        vec![Rule::RustTest]
460    }
461}
462
463impl RustTest {
464    fn common(
465        config: &GlobalConfig,
466        metadata: &PackageMetadata,
467        crate_config: &CrateConfig,
468        crate_features: List<QuotedString>,
469        name: &str,
470        kind: RustTestKind,
471        size: RustTestSize,
472    ) -> Result<Option<Self>, anyhow::Error> {
473        let test_config = crate_config.test(name);
474
475        if test_config.common().skip() {
476            return Ok(None);
477        }
478
479        let crate_name = metadata.name().to_case(Case::Snake);
480        let name = QuotedString::new(format!("{crate_name}_{}_tests", name));
481
482        // Collect all dependencies.
483        let all_deps = WorkspaceDependencies::new(config, metadata);
484        let (deps, extra_deps) = all_deps.iter(DependencyKind::Development, false);
485        let (proc_macro_deps, extra_proc_macro_deps) =
486            all_deps.iter(DependencyKind::Development, true);
487
488        let mut deps: List<QuotedString> =
489            List::new(deps).concat_other(AllCrateDeps::default().normal().normal_dev());
490        // Add extra platform deps if there are any.
491        if !extra_deps.is_empty() {
492            let select: Select<List<QuotedString>> = Select::new(extra_deps, vec![]);
493            deps = deps.concat_other(select);
494        }
495
496        let mut proc_macro_deps: List<QuotedString> = List::new(proc_macro_deps)
497            .concat_other(AllCrateDeps::default().proc_macro().proc_macro_dev());
498        // Add extra platform deps if there are any.
499        if !extra_proc_macro_deps.is_empty() {
500            let select: Select<List<QuotedString>> = Select::new(extra_proc_macro_deps, vec![]);
501            proc_macro_deps = proc_macro_deps.concat_other(select);
502        }
503
504        // TODO(parkmycar): Make the lint_config configurable.
505        let lint_config = QuotedString::new(":lints");
506
507        if matches!(kind, RustTestKind::Integration { .. }) {
508            let dep = format!(":{crate_name}");
509            if metadata.is_proc_macro() {
510                if !proc_macro_deps.iter().any(|d| d.unquoted().ends_with(&dep)) {
511                    proc_macro_deps.push_front(dep);
512                }
513            } else {
514                if !deps.iter().any(|d| d.unquoted().ends_with(&dep)) {
515                    deps.push_front(dep);
516                }
517            }
518        }
519
520        let aliases = Aliases::default()
521            .normal()
522            .normal_dev()
523            .proc_macro()
524            .proc_macro_dev();
525
526        // Extend with any extra config specified in the Cargo.toml.
527        let test_common = test_config.common();
528
529        let (paths, globs) = test_common.data();
530        let data = List::new(paths).concat_other(globs.map(Glob::new));
531
532        let (paths, globs) = test_common.compile_data();
533        let compile_data = List::new(paths).concat_other(globs.map(Glob::new));
534
535        let env = Dict::new(test_config.env());
536        let rustc_flags = List::new(test_common.rustc_flags());
537        let rustc_env = Dict::new(test_common.rustc_env());
538
539        // Use the size provided from the config, if one was provided.
540        let size = test_config.size().copied().unwrap_or(size);
541
542        Ok(Some(RustTest {
543            name: Field::new("name", name),
544            version: Field::new("version", metadata.version().to_string().into()),
545            kind,
546            features: Field::new("crate_features", crate_features),
547            aliases: Field::new("aliases", aliases),
548            lint_config: Field::new("lint_config", lint_config),
549            deps: Field::new("deps", deps),
550            proc_macro_deps: Field::new("proc_macro_deps", proc_macro_deps),
551            size: Field::new("size", size),
552            data: Field::new("data", data),
553            compile_data: Field::new("compile_data", compile_data),
554            env: Field::new("env", env),
555            rustc_flags: Field::new("rustc_flags", rustc_flags),
556            rustc_env: Field::new("rustc_env", rustc_env),
557        }))
558    }
559
560    pub fn library(
561        config: &GlobalConfig,
562        metadata: &PackageMetadata,
563        crate_config: &CrateConfig,
564        crate_features: List<QuotedString>,
565    ) -> Result<Option<Self>, anyhow::Error> {
566        let crate_name = metadata.name().to_case(Case::Snake);
567        Self::common(
568            config,
569            metadata,
570            crate_config,
571            crate_features,
572            "lib",
573            RustTestKind::library(crate_name),
574            RustTestSize::Medium,
575        )
576    }
577
578    pub fn integration(
579        config: &GlobalConfig,
580        metadata: &PackageMetadata,
581        crate_config: &CrateConfig,
582        target: &BuildTarget,
583    ) -> Result<Option<Self>, anyhow::Error> {
584        let crate_root_path = metadata
585            .manifest_path()
586            .parent()
587            .ok_or_else(|| anyhow::anyhow!("crate is at the root of the filesystem?"))?;
588        assert_eq!(
589            BuildTargetKind::Binary,
590            target.kind(),
591            "can only generate integration tests for binary build targets"
592        );
593
594        let test_target = target.path();
595        let test_target = test_target
596            .strip_prefix(crate_root_path)
597            .map_err(|_| anyhow::anyhow!("integration test is not inside workspace?"))?;
598
599        Self::common(
600            config,
601            metadata,
602            crate_config,
603            List::new::<String, _>([]),
604            target.name(),
605            RustTestKind::integration(target.name(), [test_target.to_string()]),
606            RustTestSize::Large,
607        )
608    }
609}
610
611impl ToBazelDefinition for RustTest {
612    fn format(&self, writer: &mut dyn fmt::Write) -> Result<(), fmt::Error> {
613        let mut w = AutoIndentingWriter::new(writer);
614
615        writeln!(w, "rust_test(")?;
616        {
617            let mut w = w.indent();
618
619            self.name.format(&mut w)?;
620            self.version.format(&mut w)?;
621            self.kind.format(&mut w)?;
622            self.features.format(&mut w)?;
623            self.aliases.format(&mut w)?;
624            self.lint_config.format(&mut w)?;
625            self.deps.format(&mut w)?;
626            self.proc_macro_deps.format(&mut w)?;
627            self.size.format(&mut w)?;
628
629            self.compile_data.format(&mut w)?;
630            self.data.format(&mut w)?;
631            self.env.format(&mut w)?;
632            self.rustc_flags.format(&mut w)?;
633            self.rustc_env.format(&mut w)?;
634        }
635        writeln!(w, ")")?;
636
637        Ok(())
638    }
639}
640
641#[derive(Debug)]
642pub enum RustTestKind {
643    Library(Field<QuotedString>),
644    Integration {
645        /// Name we'll give the built Rust binary.
646        ///
647        /// Some test harnesses (e.g. [insta]) use the crate name to generate
648        /// files. We provide the crate name for integration tests for parity
649        /// with cargo test.
650        ///
651        /// [insta]: https://docs.rs/insta/latest/insta/
652        test_name: Field<QuotedString>,
653        /// Source files for the integration test.
654        srcs: Field<List<QuotedString>>,
655    },
656}
657
658impl RustTestKind {
659    pub fn library(crate_name: impl Into<String>) -> Self {
660        let crate_name = QuotedString::new(format!(":{}", crate_name.into()));
661        Self::Library(Field::new("crate", crate_name))
662    }
663
664    pub fn integration(
665        test_name: impl Into<String>,
666        srcs: impl IntoIterator<Item = String>,
667    ) -> Self {
668        let test_name = test_name.into().to_case(Case::Snake);
669        let srcs = srcs.into_iter().map(QuotedString::new).collect();
670        Self::Integration {
671            test_name: Field::new("crate_name", test_name.into()),
672            srcs: Field::new("srcs", srcs),
673        }
674    }
675}
676
677impl ToBazelDefinition for RustTestKind {
678    fn format(&self, writer: &mut dyn fmt::Write) -> Result<(), fmt::Error> {
679        match self {
680            RustTestKind::Library(field) => field.format(writer)?,
681            RustTestKind::Integration { test_name, srcs } => {
682                test_name.format(writer)?;
683                srcs.format(writer)?;
684            }
685        }
686        Ok(())
687    }
688}
689
690/// Size of the Bazel Test.
691///
692/// <https://bazel.build/reference/be/common-definitions#common-attributes-tests>
693#[derive(Debug, Clone, Copy, Default)]
694pub enum RustTestSize {
695    Small,
696    Medium,
697    #[default]
698    Large,
699    Enormous,
700}
701
702impl ToBazelDefinition for RustTestSize {
703    fn format(&self, writer: &mut dyn fmt::Write) -> Result<(), fmt::Error> {
704        use RustTestSize::*;
705        let s = match self {
706            Small => "small",
707            Medium => "medium",
708            Large => "large",
709            Enormous => "enormous",
710        };
711        write!(writer, "\"{s}\"")
712    }
713}
714
715impl FromStr for RustTestSize {
716    type Err = String;
717
718    fn from_str(s: &str) -> Result<Self, Self::Err> {
719        let size = match s {
720            "small" => RustTestSize::Small,
721            "medium" => RustTestSize::Medium,
722            "large" => RustTestSize::Large,
723            "enormous" => RustTestSize::Enormous,
724            other => return Err(other.to_string()),
725        };
726        Ok(size)
727    }
728}
729
730impl<'de> serde::Deserialize<'de> for RustTestSize {
731    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
732    where
733        D: serde::Deserializer<'de>,
734    {
735        let s = String::deserialize(deserializer)?;
736        RustTestSize::from_str(&s).map_err(serde::de::Error::custom)
737    }
738}
739
740/// [`rust_doc_test`](http://bazelbuild.github.io/rules_rust/rust_doc.html#rust_doc_test).
741#[derive(Debug)]
742pub struct RustDocTest {
743    name: Field<QuotedString>,
744    crate_: Field<QuotedString>,
745    deps: Field<List<QuotedString>>,
746}
747
748impl RustDocTest {
749    pub fn generate(
750        config: &GlobalConfig,
751        metadata: &PackageMetadata,
752        crate_config: &CrateConfig,
753    ) -> Result<Option<Self>, anyhow::Error> {
754        if crate_config.doc_test().common().skip() {
755            return Ok(None);
756        }
757
758        let crate_name = metadata.name().to_case(Case::Snake);
759        let name = QuotedString::new(format!("{crate_name}_doc_test"));
760        let crate_ = QuotedString::new(format!(":{crate_name}"));
761
762        // Collect all dependencies.
763        let all_deps = WorkspaceDependencies::new(config, metadata);
764        let (deps, extra_deps) = all_deps.iter(DependencyKind::Development, false);
765        let mut deps: List<QuotedString> =
766            List::new(deps).concat_other(AllCrateDeps::default().normal().normal_dev());
767
768        // Add extra platform deps if there are any.
769        if !extra_deps.is_empty() {
770            let select: Select<List<QuotedString>> = Select::new(extra_deps, vec![]);
771            deps = deps.concat_other(select);
772        }
773
774        Ok(Some(RustDocTest {
775            name: Field::new("name", name),
776            crate_: Field::new("crate", crate_),
777            deps: Field::new("deps", deps),
778        }))
779    }
780}
781
782impl ToBazelDefinition for RustDocTest {
783    fn format(&self, writer: &mut dyn fmt::Write) -> Result<(), fmt::Error> {
784        let mut w = AutoIndentingWriter::new(writer);
785
786        writeln!(w, "rust_doc_test(")?;
787        {
788            let mut w = w.indent();
789            self.name.format(&mut w)?;
790            self.crate_.format(&mut w)?;
791            self.deps.format(&mut w)?;
792        }
793        writeln!(w, ")")?;
794
795        Ok(())
796    }
797}
798
799/// [`cargo_build_script`](http://bazelbuild.github.io/rules_rust/cargo.html#cargo_build_script)
800#[derive(Debug)]
801pub struct CargoBuildScript {
802    pub name: Field<QuotedString>,
803    pub script_src: Field<List<QuotedString>>,
804    pub deps: Field<List<QuotedString>>,
805    pub proc_macro_deps: Field<List<QuotedString>>,
806    pub build_script_env: Field<Dict<QuotedString, QuotedString>>,
807    pub data: Field<List<QuotedString>>,
808    pub compile_data: Field<List<QuotedString>>,
809    pub rustc_flags: Field<List<QuotedString>>,
810    pub rustc_env: Field<Dict<QuotedString, QuotedString>>,
811    pub extras: Vec<Box<dyn ToBazelDefinition>>,
812}
813
814impl RustTarget for CargoBuildScript {
815    fn rules(&self) -> Vec<Rule> {
816        vec![Rule::CargoBuildScript]
817    }
818}
819
820impl CargoBuildScript {
821    pub fn generate(
822        config: &GlobalConfig,
823        context: &CrateContext,
824        crate_config: &CrateConfig,
825        metadata: &PackageMetadata,
826    ) -> Result<Option<Self>, anyhow::Error> {
827        let crate_name = metadata.name().to_case(Case::Snake);
828        let name = QuotedString::new(format!("{crate_name}_build_script"));
829
830        // Determine the source for this build script.
831        let Some(build_script_target) = metadata.build_target(&BuildTargetId::BuildScript) else {
832            return Ok(None);
833        };
834
835        // Build scripts _should_ only ever exist at `build.rs`, but guard
836        // against them existing somewhere else.
837        let crate_root_path = metadata
838            .manifest_path()
839            .parent()
840            .ok_or_else(|| anyhow::anyhow!("package is at the root of the filesystem?"))?;
841        let script_src = build_script_target
842            .path()
843            .strip_prefix(crate_root_path)
844            .map_err(|_| anyhow::anyhow!("build script is not inside of crate"))?;
845        let script_src = Field::new(
846            "srcs",
847            List::new(vec![QuotedString::new(script_src.to_string())]),
848        );
849
850        // Collect all dependencies.
851        let all_deps = WorkspaceDependencies::new(config, metadata);
852        let (deps, extra_deps) = all_deps.iter(DependencyKind::Build, false);
853        let (proc_macro_deps, extra_proc_macro_deps) =
854            all_deps.iter(DependencyKind::Development, true);
855
856        let mut deps: List<QuotedString> =
857            List::new(deps).concat_other(AllCrateDeps::default().build());
858        // Add extra platform deps if there are any.
859        if !extra_deps.is_empty() {
860            let select: Select<List<QuotedString>> = Select::new(extra_deps, vec![]);
861            deps = deps.concat_other(select);
862        }
863
864        let mut proc_macro_deps: List<QuotedString> =
865            List::new(proc_macro_deps).concat_other(AllCrateDeps::default().build_proc_macro());
866        // Add extra platform deps if there are any.
867        if !extra_proc_macro_deps.is_empty() {
868            let select: Select<List<QuotedString>> = Select::new(extra_proc_macro_deps, vec![]);
869            proc_macro_deps = proc_macro_deps.concat_other(select);
870        }
871
872        // Generate any extra targets that we need.
873        let mut extras: Vec<Box<dyn ToBazelDefinition>> = Vec::new();
874        let mut data: List<QuotedString> = List::empty();
875
876        // Generate a filegroup for any files this build script depends on.
877        let mut protos = context
878            .build_script
879            .as_ref()
880            .map(|b| b.generated_protos.clone())
881            .unwrap_or_default();
882
883        // Make sure to add transitive dependencies.
884        let crate_filename = crate_root_path
885            .file_name()
886            .ok_or_else(|| anyhow::anyhow!("crate is at the root of the filesystem?"))?;
887        let proto_dependencies = context.build_script.as_ref().map(|b| &b.proto_dependencies);
888        if let Some(deps) = proto_dependencies {
889            let transitive_deps: BTreeSet<_> = deps
890                .iter()
891                // Google related dependencies are included via "well known types".
892                .filter(|p| !p.starts_with("google"))
893                // Imports from within the same crate are already included.
894                .filter(|p| !p.starts_with(crate_filename))
895                // This assume the root of the protobuf path is a crate, which might not be true?
896                .map(|p| p.components().next().unwrap())
897                // TODO(parkmcar): This is a bit hacky, we need to consider where
898                // we are relative to the workspace root.
899                .map(|name| format!("//src/{name}:{PROTO_FILEGROUP_NAME}"))
900                // Collect into a `BTreeSet` to de-dupe.
901                .collect();
902
903            protos.extend(transitive_deps);
904        }
905
906        if !protos.is_empty() {
907            let proto_filegroup = FileGroup::new(PROTO_FILEGROUP_NAME, protos);
908            extras.push(Box::new(proto_filegroup));
909
910            // Make sure to include this file group in the build script data!
911            data.push_back(format!(":{PROTO_FILEGROUP_NAME}"));
912        }
913
914        // Check for any metadata specified in the Cargo.toml.
915        let build_common = crate_config.build().common();
916
917        let (paths, globs) = build_common.data();
918        data.extend(paths);
919        let data = data.concat_other(globs.map(Glob::new));
920
921        let (paths, globs) = build_common.compile_data();
922        let compile_data = List::new(paths).concat_other(globs.map(Glob::new));
923
924        let rustc_flags = List::new(build_common.rustc_flags());
925        let rustc_env = Dict::new(build_common.rustc_env());
926        let build_script_env = Dict::new(crate_config.build().build_script_env());
927
928        Ok(Some(CargoBuildScript {
929            name: Field::new("name", name),
930            script_src,
931            deps: Field::new("deps", deps),
932            proc_macro_deps: Field::new("proc_macro_deps", proc_macro_deps),
933            build_script_env: Field::new("build_script_env", build_script_env),
934            data: Field::new("data", data),
935            compile_data: Field::new("compile_data", compile_data),
936            rustc_flags: Field::new("rustc_flags", rustc_flags),
937            rustc_env: Field::new("rustc_env", rustc_env),
938            extras,
939        }))
940    }
941}
942
943impl ToBazelDefinition for CargoBuildScript {
944    fn format(&self, writer: &mut dyn fmt::Write) -> Result<(), fmt::Error> {
945        let mut w = AutoIndentingWriter::new(writer);
946
947        // Write any extra targets this build script depends on.
948        for extra in &self.extras {
949            extra.format(&mut w)?;
950            writeln!(w)?;
951        }
952
953        writeln!(w, "cargo_build_script(")?;
954        {
955            let mut w = w.indent();
956
957            self.name.format(&mut w)?;
958            self.script_src.format(&mut w)?;
959            self.deps.format(&mut w)?;
960            self.proc_macro_deps.format(&mut w)?;
961            self.build_script_env.format(&mut w)?;
962
963            self.data.format(&mut w)?;
964            self.compile_data.format(&mut w)?;
965            self.rustc_flags.format(&mut w)?;
966            self.rustc_env.format(&mut w)?;
967        }
968        writeln!(w, ")")?;
969
970        Ok(())
971    }
972}
973
974/// Reads lint config from a Cargo.toml file.
975#[derive(Debug)]
976pub struct ExtractCargoLints {
977    name: Field<QuotedString>,
978    manifest: Field<QuotedString>,
979    workspace: Field<QuotedString>,
980}
981
982impl RustTarget for ExtractCargoLints {
983    fn rules(&self) -> Vec<Rule> {
984        vec![Rule::ExtractCargoLints]
985    }
986}
987
988impl ExtractCargoLints {
989    pub fn generate() -> Self {
990        ExtractCargoLints {
991            name: Field::new("name", QuotedString::new("lints")),
992            manifest: Field::new("manifest", QuotedString::new("Cargo.toml")),
993            workspace: Field::new("workspace", QuotedString::new("@//:Cargo.toml")),
994        }
995    }
996}
997
998impl ToBazelDefinition for ExtractCargoLints {
999    fn format(&self, writer: &mut dyn fmt::Write) -> Result<(), fmt::Error> {
1000        let mut w = AutoIndentingWriter::new(writer);
1001
1002        writeln!(w, "extract_cargo_lints(")?;
1003        {
1004            let mut w = w.indent();
1005
1006            self.name.format(&mut w)?;
1007            self.manifest.format(&mut w)?;
1008            self.workspace.format(&mut w)?;
1009        }
1010        writeln!(w, ")")?;
1011
1012        Ok(())
1013    }
1014}
1015
1016/// An opaque blob of text that we treat as a target.
1017#[derive(Debug)]
1018pub struct AdditiveContent(String);
1019
1020impl AdditiveContent {
1021    pub fn new(s: &str) -> Self {
1022        AdditiveContent(s.to_string())
1023    }
1024}
1025
1026impl ToBazelDefinition for AdditiveContent {
1027    fn format(&self, writer: &mut dyn fmt::Write) -> Result<(), fmt::Error> {
1028        writeln!(writer, "{}", self.0)?;
1029        Ok(())
1030    }
1031}
1032
1033impl RustTarget for AdditiveContent {
1034    fn rules(&self) -> Vec<Rule> {
1035        vec![]
1036    }
1037}
1038
1039type AllCrateDeps = CratesUniverseMacro<AllCrateDeps_>;
1040type Aliases = CratesUniverseMacro<Aliases_>;
1041
1042/// [`crates_universe`](http://bazelbuild.github.io/rules_rust/crate_universe.html) exposes a few
1043/// macros that make it easier to define depedencies and aliases.
1044#[derive(Default, Debug)]
1045struct CratesUniverseMacro<Name> {
1046    name: Name,
1047    fields: Vec<MacroOption>,
1048}
1049
1050impl<Name> CratesUniverseMacro<Name> {
1051    pub fn normal(mut self) -> Self {
1052        self.fields.push(MacroOption::Normal);
1053        self
1054    }
1055
1056    pub fn proc_macro(mut self) -> Self {
1057        self.fields.push(MacroOption::ProcMacro);
1058        self
1059    }
1060
1061    pub fn normal_dev(mut self) -> Self {
1062        self.fields.push(MacroOption::NormalDev);
1063        self
1064    }
1065
1066    pub fn proc_macro_dev(mut self) -> Self {
1067        self.fields.push(MacroOption::ProcMacroDev);
1068        self
1069    }
1070
1071    pub fn build(mut self) -> Self {
1072        self.fields.push(MacroOption::Build);
1073        self
1074    }
1075
1076    pub fn build_proc_macro(mut self) -> Self {
1077        self.fields.push(MacroOption::BuildProcMacro);
1078        self
1079    }
1080}
1081
1082impl<N: Named> ToBazelDefinition for CratesUniverseMacro<N> {
1083    fn format(&self, writer: &mut dyn fmt::Write) -> Result<(), fmt::Error> {
1084        let mut w = AutoIndentingWriter::new(writer);
1085
1086        write!(w, "{}", self.name.name())?;
1087
1088        match &self.fields[..] {
1089            [] => write!(w, "()")?,
1090            [one] => write!(w, "({} = True)", one.to_bazel_definition())?,
1091            multiple => {
1092                write!(w, "(")?;
1093                for item in multiple {
1094                    let mut w = w.indent();
1095                    writeln!(w)?;
1096                    write!(w, "{} = True,", item.to_bazel_definition())?;
1097                }
1098                write!(w, "\n)")?;
1099            }
1100        }
1101
1102        Ok(())
1103    }
1104}
1105
1106#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)]
1107enum MacroOption {
1108    Normal,
1109    ProcMacro,
1110    NormalDev,
1111    ProcMacroDev,
1112    Build,
1113    BuildProcMacro,
1114}
1115
1116impl ToBazelDefinition for MacroOption {
1117    fn format(&self, writer: &mut dyn fmt::Write) -> Result<(), fmt::Error> {
1118        match self {
1119            MacroOption::Normal => write!(writer, "normal"),
1120            MacroOption::ProcMacro => write!(writer, "proc_macro"),
1121            MacroOption::NormalDev => write!(writer, "normal_dev"),
1122            MacroOption::ProcMacroDev => write!(writer, "proc_macro_dev"),
1123            MacroOption::Build => write!(writer, "build"),
1124            MacroOption::BuildProcMacro => write!(writer, "build_proc_macro"),
1125        }
1126    }
1127}
1128
1129/// A hack for const generic strings.
1130trait Named: std::fmt::Debug {
1131    fn name(&self) -> &'static str;
1132}
1133#[derive(Default, Debug)]
1134struct AllCrateDeps_;
1135impl Named for AllCrateDeps_ {
1136    fn name(&self) -> &'static str {
1137        "all_crate_deps"
1138    }
1139}
1140#[derive(Default, Debug)]
1141struct Aliases_;
1142impl Named for Aliases_ {
1143    fn name(&self) -> &'static str {
1144        "aliases"
1145    }
1146}
1147
1148struct WorkspaceDependencies<'a> {
1149    config: &'a GlobalConfig,
1150    package: &'a PackageMetadata<'a>,
1151}
1152
1153impl<'a> WorkspaceDependencies<'a> {
1154    pub fn new(config: &'a GlobalConfig, package: &'a PackageMetadata<'a>) -> Self {
1155        WorkspaceDependencies { config, package }
1156    }
1157
1158    /// Returns a set of dependencies that are common to all platforms, and
1159    /// then any additional dependencies that need to be enabled for a specific
1160    /// platform.
1161    pub fn iter(
1162        &self,
1163        kind: DependencyKind,
1164        proc_macro: bool,
1165    ) -> (BTreeSet<String>, BTreeMap<PlatformVariant, Vec<String>>) {
1166        let feature_set = platform_feature_sets(self.package);
1167
1168        let dependencies: BTreeMap<_, _> = feature_set
1169            .into_iter()
1170            .map(|(platform, feature_set)| {
1171                // Convert the feature set to the set of packages it enables.
1172                let deps: BTreeSet<_> = feature_set
1173                    .to_package_set()
1174                    .links(DependencyDirection::Reverse)
1175                    // Filter down to only direct dependencies.
1176                    .filter(|link| link.from().id() == self.package.id())
1177                    .filter(move |link| match kind {
1178                        DependencyKind::Build | DependencyKind::Normal => {
1179                            link.req_for_kind(kind).is_present()
1180                        }
1181                        // Tests can rely on normal dependencies, so also check those.
1182                        DependencyKind::Development => {
1183                            link.req_for_kind(kind).is_present()
1184                                || link.req_for_kind(DependencyKind::Normal).is_present()
1185                        }
1186                    })
1187                    .map(|link| link.to())
1188                    // Ignore deps filtered out by the global config.
1189                    .filter(|meta| self.config.include_dep(meta.name()))
1190                    // Filter proc_macro deps.
1191                    .filter(move |meta| meta.is_proc_macro() == proc_macro)
1192                    // Filter map down to only deps in the workspace, and their path.
1193                    .filter_map(|meta| meta.source().workspace_path().map(|p| (p, meta)))
1194                    .map(|(path, meta)| {
1195                        let crate_name = meta.name().to_case(Case::Snake);
1196                        format!("//{}:{}", path, crate_name)
1197                    })
1198                    .collect();
1199
1200                (platform, deps)
1201            })
1202            .collect();
1203
1204        // Dependencies that are common to all platforms.
1205        let common = dependencies
1206            .iter()
1207            .fold(None, |common, (_variant, set)| match common {
1208                None => Some(set.clone()),
1209                Some(common) => Some(common.intersection(set).cloned().collect()),
1210            })
1211            .unwrap_or_default();
1212
1213        // Extra features for each platform that need to be enabled.
1214        let extras: BTreeMap<_, _> = dependencies
1215            .into_iter()
1216            .filter_map(|(variant, features)| {
1217                let extra: Vec<_> = features.difference(&common).cloned().collect();
1218                if extra.is_empty() {
1219                    None
1220                } else {
1221                    Some((variant, extra))
1222                }
1223            })
1224            .collect();
1225
1226        (common, extras)
1227    }
1228}
1229
1230/// Returns a set of Cargo features that are common to all platforms, and then
1231/// any additional features that need to be enabled for a specific platform.
1232pub fn crate_features<'a>(
1233    config: &'a GlobalConfig,
1234    package: &'a PackageMetadata<'a>,
1235) -> Result<(BTreeSet<String>, BTreeMap<PlatformVariant, Vec<String>>), anyhow::Error> {
1236    // Resolve feature sets for all of the platforms we care about.
1237    let feature_sets = platform_feature_sets(package);
1238
1239    // Filter down to just the feature labels for this crate.
1240    let features: BTreeMap<_, _> = feature_sets
1241        .into_iter()
1242        .map(|(platform, feature_set)| {
1243            // Filter down to the features for just this crate.
1244            let features: BTreeSet<_> = feature_set
1245                .features_for(package.id())
1246                .expect("package id should be known")
1247                .expect("package id should be in the feature set")
1248                .into_iter()
1249                .filter_map(|feature| match feature.label() {
1250                    FeatureLabel::Base => None,
1251                    FeatureLabel::Named(f) => Some(f),
1252                    FeatureLabel::OptionalDependency(f) => Some(f),
1253                })
1254                // TODO(parkmycar): We shouldn't ignore features based on name, but if
1255                // enabling that feature would result in us depending on a crate we
1256                // want to ignore.
1257                .filter(|name| config.include_dep(name))
1258                .map(|s| s.to_string())
1259                .collect();
1260
1261            (platform, features)
1262        })
1263        .collect();
1264
1265    // Features that are common to all platforms.
1266    let common = features
1267        .iter()
1268        .fold(None, |common, (_variant, set)| match common {
1269            None => Some(set.clone()),
1270            Some(common) => Some(common.intersection(set).cloned().collect()),
1271        })
1272        .unwrap_or_default();
1273
1274    // Extra features for each platform that need to be enabled.
1275    let extras: BTreeMap<_, _> = features
1276        .into_iter()
1277        .filter_map(|(variant, features)| {
1278            let extra: Vec<_> = features.difference(&common).cloned().collect();
1279            if extra.is_empty() {
1280                None
1281            } else {
1282                Some((variant, extra))
1283            }
1284        })
1285        .collect();
1286
1287    Ok((common, extras))
1288}
1289
1290/// Returns a [`FeatureSet`] of reverse dependencies (in other words, all
1291/// crates that depend on) for the provided package, for every platform that we
1292/// support.
1293///
1294/// TODO(parkmycar): Make the list of platforms configurable.
1295pub fn platform_feature_sets<'a>(
1296    package: &'a PackageMetadata<'a>,
1297) -> BTreeMap<PlatformVariant, FeatureSet<'a>> {
1298    // Resolve a feature graph for all crates that depend on this one.
1299    let dependents = package
1300        .to_package_query(DependencyDirection::Reverse)
1301        .resolve()
1302        .to_feature_set(StandardFeatures::Default);
1303
1304    PlatformVariant::all()
1305        .iter()
1306        .map(|p| {
1307            // Resolve all features enabled for the specified platform.
1308            let feature_set = dependents
1309                .to_feature_query(DependencyDirection::Forward)
1310                .resolve_with_fn(|_query, cond_link| {
1311                    // Note: We don't currently generate different targets for
1312                    // dev or build dependencies, but possibly could if need be.
1313                    let normal_enabled = cond_link.normal().enabled_on(p.spec());
1314                    let dev_enabled = cond_link.dev().enabled_on(p.spec());
1315                    let build_enabled = cond_link.build().enabled_on(p.spec());
1316
1317                    matches!(normal_enabled, EnabledTernary::Enabled)
1318                        || matches!(dev_enabled, EnabledTernary::Enabled)
1319                        || matches!(build_enabled, EnabledTernary::Enabled)
1320                });
1321
1322            (*p, feature_set)
1323        })
1324        .collect()
1325}