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::{ConfigSettingGroup, 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 xlang_lto_select: Select<List<QuotedString>> = Select::new(
386            [(
387                ConfigSettingGroup::XlangLtoEnabled,
388                vec![QuotedString::new("-Clinker-plugin-lto")],
389            )],
390            vec![],
391        );
392
393        let env = Dict::new(bin_config.env());
394        let rustc_flags =
395            List::new(bin_config.common().rustc_flags()).concat_other(xlang_lto_select);
396        let rustc_env = Dict::new(bin_config.common().rustc_env());
397
398        Ok(Some(RustBinary {
399            name: Field::new("name", target_name),
400            version: Field::new("version", metadata.version().to_string().into()),
401            crate_root: Field::new("crate_root", binary_path),
402            features: Field::new("features", List::empty()),
403            aliases: Field::new("aliases", Aliases::default().normal().proc_macro()),
404            lint_config: Field::new("lint_config", lint_config),
405            deps: Field::new("deps", deps),
406            proc_macro_deps: Field::new("proc_macro_deps", proc_macro_deps),
407            data: Field::new("data", data),
408            compile_data: Field::new("compile_data", compile_data),
409            env: Field::new("env", env),
410            rustc_flags: Field::new("rustc_flags", rustc_flags),
411            rustc_env: Field::new("rustc_env", rustc_env),
412        }))
413    }
414}
415
416impl ToBazelDefinition for RustBinary {
417    fn format(&self, w: &mut dyn fmt::Write) -> Result<(), fmt::Error> {
418        let mut w = AutoIndentingWriter::new(w);
419
420        writeln!(w, "rust_binary(")?;
421        {
422            let mut w = w.indent();
423
424            self.name.format(&mut w)?;
425            self.version.format(&mut w)?;
426            self.crate_root.format(&mut w)?;
427
428            writeln!(w, r#"srcs = glob(["src/**/*.rs"]),"#)?;
429
430            self.features.format(&mut w)?;
431            self.aliases.format(&mut w)?;
432            self.lint_config.format(&mut w)?;
433            self.deps.format(&mut w)?;
434            self.proc_macro_deps.format(&mut w)?;
435            self.compile_data.format(&mut w)?;
436            self.data.format(&mut w)?;
437            self.env.format(&mut w)?;
438            self.rustc_flags.format(&mut w)?;
439            self.rustc_env.format(&mut w)?;
440        }
441        writeln!(w, ")")?;
442
443        Ok(())
444    }
445}
446
447/// [`rust_test`](https://bazelbuild.github.io/rules_rust/defs.html#rust_test)
448#[derive(Debug)]
449pub struct RustTest {
450    name: Field<QuotedString>,
451    version: Field<QuotedString>,
452    kind: RustTestKind,
453    features: Field<List<QuotedString>>,
454    aliases: Field<Aliases>,
455    lint_config: Field<QuotedString>,
456    deps: Field<List<QuotedString>>,
457    proc_macro_deps: Field<List<QuotedString>>,
458    size: Field<RustTestSize>,
459    data: Field<List<QuotedString>>,
460    compile_data: Field<List<QuotedString>>,
461    env: Field<Dict<QuotedString, QuotedString>>,
462    rustc_flags: Field<List<QuotedString>>,
463    rustc_env: Field<Dict<QuotedString, QuotedString>>,
464}
465
466impl RustTarget for RustTest {
467    fn rules(&self) -> Vec<Rule> {
468        vec![Rule::RustTest]
469    }
470}
471
472impl RustTest {
473    fn common(
474        config: &GlobalConfig,
475        metadata: &PackageMetadata,
476        crate_config: &CrateConfig,
477        crate_features: List<QuotedString>,
478        name: &str,
479        kind: RustTestKind,
480        size: RustTestSize,
481    ) -> Result<Option<Self>, anyhow::Error> {
482        let test_config = crate_config.test(name);
483
484        if test_config.common().skip() {
485            return Ok(None);
486        }
487
488        let crate_name = metadata.name().to_case(Case::Snake);
489        let name = QuotedString::new(format!("{crate_name}_{}_tests", name));
490
491        // Collect all dependencies.
492        let all_deps = WorkspaceDependencies::new(config, metadata);
493        let (deps, extra_deps) = all_deps.iter(DependencyKind::Development, false);
494        let (proc_macro_deps, extra_proc_macro_deps) =
495            all_deps.iter(DependencyKind::Development, true);
496
497        let mut deps: List<QuotedString> =
498            List::new(deps).concat_other(AllCrateDeps::default().normal().normal_dev());
499        // Add extra platform deps if there are any.
500        if !extra_deps.is_empty() {
501            let select: Select<List<QuotedString>> = Select::new(extra_deps, vec![]);
502            deps = deps.concat_other(select);
503        }
504
505        let mut proc_macro_deps: List<QuotedString> = List::new(proc_macro_deps)
506            .concat_other(AllCrateDeps::default().proc_macro().proc_macro_dev());
507        // Add extra platform deps if there are any.
508        if !extra_proc_macro_deps.is_empty() {
509            let select: Select<List<QuotedString>> = Select::new(extra_proc_macro_deps, vec![]);
510            proc_macro_deps = proc_macro_deps.concat_other(select);
511        }
512
513        // TODO(parkmycar): Make the lint_config configurable.
514        let lint_config = QuotedString::new(":lints");
515
516        if matches!(kind, RustTestKind::Integration { .. }) {
517            let dep = format!(":{crate_name}");
518            if metadata.is_proc_macro() {
519                if !proc_macro_deps.iter().any(|d| d.unquoted().ends_with(&dep)) {
520                    proc_macro_deps.push_front(dep);
521                }
522            } else {
523                if !deps.iter().any(|d| d.unquoted().ends_with(&dep)) {
524                    deps.push_front(dep);
525                }
526            }
527        }
528
529        let aliases = Aliases::default()
530            .normal()
531            .normal_dev()
532            .proc_macro()
533            .proc_macro_dev();
534
535        // Extend with any extra config specified in the Cargo.toml.
536        let test_common = test_config.common();
537
538        let (paths, globs) = test_common.data();
539        let data = List::new(paths).concat_other(globs.map(Glob::new));
540
541        let (paths, globs) = test_common.compile_data();
542        let compile_data = List::new(paths).concat_other(globs.map(Glob::new));
543
544        let env = Dict::new(test_config.env());
545        let rustc_flags = List::new(test_common.rustc_flags());
546        let rustc_env = Dict::new(test_common.rustc_env());
547
548        // Use the size provided from the config, if one was provided.
549        let size = test_config.size().copied().unwrap_or(size);
550
551        Ok(Some(RustTest {
552            name: Field::new("name", name),
553            version: Field::new("version", metadata.version().to_string().into()),
554            kind,
555            features: Field::new("crate_features", crate_features),
556            aliases: Field::new("aliases", aliases),
557            lint_config: Field::new("lint_config", lint_config),
558            deps: Field::new("deps", deps),
559            proc_macro_deps: Field::new("proc_macro_deps", proc_macro_deps),
560            size: Field::new("size", size),
561            data: Field::new("data", data),
562            compile_data: Field::new("compile_data", compile_data),
563            env: Field::new("env", env),
564            rustc_flags: Field::new("rustc_flags", rustc_flags),
565            rustc_env: Field::new("rustc_env", rustc_env),
566        }))
567    }
568
569    pub fn library(
570        config: &GlobalConfig,
571        metadata: &PackageMetadata,
572        crate_config: &CrateConfig,
573        crate_features: List<QuotedString>,
574    ) -> Result<Option<Self>, anyhow::Error> {
575        let crate_name = metadata.name().to_case(Case::Snake);
576        Self::common(
577            config,
578            metadata,
579            crate_config,
580            crate_features,
581            "lib",
582            RustTestKind::library(crate_name),
583            RustTestSize::Medium,
584        )
585    }
586
587    pub fn integration(
588        config: &GlobalConfig,
589        metadata: &PackageMetadata,
590        crate_config: &CrateConfig,
591        target: &BuildTarget,
592    ) -> Result<Option<Self>, anyhow::Error> {
593        let crate_root_path = metadata
594            .manifest_path()
595            .parent()
596            .ok_or_else(|| anyhow::anyhow!("crate is at the root of the filesystem?"))?;
597        assert_eq!(
598            BuildTargetKind::Binary,
599            target.kind(),
600            "can only generate integration tests for binary build targets"
601        );
602
603        let test_target = target.path();
604        let test_target = test_target
605            .strip_prefix(crate_root_path)
606            .map_err(|_| anyhow::anyhow!("integration test is not inside workspace?"))?;
607
608        Self::common(
609            config,
610            metadata,
611            crate_config,
612            List::new::<String, _>([]),
613            target.name(),
614            RustTestKind::integration(target.name(), [test_target.to_string()]),
615            RustTestSize::Large,
616        )
617    }
618}
619
620impl ToBazelDefinition for RustTest {
621    fn format(&self, writer: &mut dyn fmt::Write) -> Result<(), fmt::Error> {
622        let mut w = AutoIndentingWriter::new(writer);
623
624        writeln!(w, "rust_test(")?;
625        {
626            let mut w = w.indent();
627
628            self.name.format(&mut w)?;
629            self.version.format(&mut w)?;
630            self.kind.format(&mut w)?;
631            self.features.format(&mut w)?;
632            self.aliases.format(&mut w)?;
633            self.lint_config.format(&mut w)?;
634            self.deps.format(&mut w)?;
635            self.proc_macro_deps.format(&mut w)?;
636            self.size.format(&mut w)?;
637
638            self.compile_data.format(&mut w)?;
639            self.data.format(&mut w)?;
640            self.env.format(&mut w)?;
641            self.rustc_flags.format(&mut w)?;
642            self.rustc_env.format(&mut w)?;
643        }
644        writeln!(w, ")")?;
645
646        Ok(())
647    }
648}
649
650#[derive(Debug)]
651pub enum RustTestKind {
652    Library(Field<QuotedString>),
653    Integration {
654        /// Name we'll give the built Rust binary.
655        ///
656        /// Some test harnesses (e.g. [insta]) use the crate name to generate
657        /// files. We provide the crate name for integration tests for parity
658        /// with cargo test.
659        ///
660        /// [insta]: https://docs.rs/insta/latest/insta/
661        test_name: Field<QuotedString>,
662        /// Source files for the integration test.
663        srcs: Field<List<QuotedString>>,
664    },
665}
666
667impl RustTestKind {
668    pub fn library(crate_name: impl Into<String>) -> Self {
669        let crate_name = QuotedString::new(format!(":{}", crate_name.into()));
670        Self::Library(Field::new("crate", crate_name))
671    }
672
673    pub fn integration(
674        test_name: impl Into<String>,
675        srcs: impl IntoIterator<Item = String>,
676    ) -> Self {
677        let test_name = test_name.into().to_case(Case::Snake);
678        let srcs = srcs.into_iter().map(QuotedString::new).collect();
679        Self::Integration {
680            test_name: Field::new("crate_name", test_name.into()),
681            srcs: Field::new("srcs", srcs),
682        }
683    }
684}
685
686impl ToBazelDefinition for RustTestKind {
687    fn format(&self, writer: &mut dyn fmt::Write) -> Result<(), fmt::Error> {
688        match self {
689            RustTestKind::Library(field) => field.format(writer)?,
690            RustTestKind::Integration { test_name, srcs } => {
691                test_name.format(writer)?;
692                srcs.format(writer)?;
693            }
694        }
695        Ok(())
696    }
697}
698
699/// Size of the Bazel Test.
700///
701/// <https://bazel.build/reference/be/common-definitions#common-attributes-tests>
702#[derive(Debug, Clone, Copy, Default)]
703pub enum RustTestSize {
704    Small,
705    Medium,
706    #[default]
707    Large,
708    Enormous,
709}
710
711impl ToBazelDefinition for RustTestSize {
712    fn format(&self, writer: &mut dyn fmt::Write) -> Result<(), fmt::Error> {
713        use RustTestSize::*;
714        let s = match self {
715            Small => "small",
716            Medium => "medium",
717            Large => "large",
718            Enormous => "enormous",
719        };
720        write!(writer, "\"{s}\"")
721    }
722}
723
724impl FromStr for RustTestSize {
725    type Err = String;
726
727    fn from_str(s: &str) -> Result<Self, Self::Err> {
728        let size = match s {
729            "small" => RustTestSize::Small,
730            "medium" => RustTestSize::Medium,
731            "large" => RustTestSize::Large,
732            "enormous" => RustTestSize::Enormous,
733            other => return Err(other.to_string()),
734        };
735        Ok(size)
736    }
737}
738
739impl<'de> serde::Deserialize<'de> for RustTestSize {
740    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
741    where
742        D: serde::Deserializer<'de>,
743    {
744        let s = String::deserialize(deserializer)?;
745        RustTestSize::from_str(&s).map_err(serde::de::Error::custom)
746    }
747}
748
749/// [`rust_doc_test`](http://bazelbuild.github.io/rules_rust/rust_doc.html#rust_doc_test).
750#[derive(Debug)]
751pub struct RustDocTest {
752    name: Field<QuotedString>,
753    crate_: Field<QuotedString>,
754    deps: Field<List<QuotedString>>,
755}
756
757impl RustDocTest {
758    pub fn generate(
759        config: &GlobalConfig,
760        metadata: &PackageMetadata,
761        crate_config: &CrateConfig,
762    ) -> Result<Option<Self>, anyhow::Error> {
763        if crate_config.doc_test().common().skip() {
764            return Ok(None);
765        }
766
767        let crate_name = metadata.name().to_case(Case::Snake);
768        let name = QuotedString::new(format!("{crate_name}_doc_test"));
769        let crate_ = QuotedString::new(format!(":{crate_name}"));
770
771        // Collect all dependencies.
772        let all_deps = WorkspaceDependencies::new(config, metadata);
773        let (deps, extra_deps) = all_deps.iter(DependencyKind::Development, false);
774        let mut deps: List<QuotedString> =
775            List::new(deps).concat_other(AllCrateDeps::default().normal().normal_dev());
776
777        // Add extra platform deps if there are any.
778        if !extra_deps.is_empty() {
779            let select: Select<List<QuotedString>> = Select::new(extra_deps, vec![]);
780            deps = deps.concat_other(select);
781        }
782
783        Ok(Some(RustDocTest {
784            name: Field::new("name", name),
785            crate_: Field::new("crate", crate_),
786            deps: Field::new("deps", deps),
787        }))
788    }
789}
790
791impl ToBazelDefinition for RustDocTest {
792    fn format(&self, writer: &mut dyn fmt::Write) -> Result<(), fmt::Error> {
793        let mut w = AutoIndentingWriter::new(writer);
794
795        writeln!(w, "rust_doc_test(")?;
796        {
797            let mut w = w.indent();
798            self.name.format(&mut w)?;
799            self.crate_.format(&mut w)?;
800            self.deps.format(&mut w)?;
801        }
802        writeln!(w, ")")?;
803
804        Ok(())
805    }
806}
807
808/// [`cargo_build_script`](http://bazelbuild.github.io/rules_rust/cargo.html#cargo_build_script)
809#[derive(Debug)]
810pub struct CargoBuildScript {
811    pub name: Field<QuotedString>,
812    pub script_src: Field<List<QuotedString>>,
813    pub deps: Field<List<QuotedString>>,
814    pub proc_macro_deps: Field<List<QuotedString>>,
815    pub build_script_env: Field<Dict<QuotedString, QuotedString>>,
816    pub data: Field<List<QuotedString>>,
817    pub compile_data: Field<List<QuotedString>>,
818    pub rustc_flags: Field<List<QuotedString>>,
819    pub rustc_env: Field<Dict<QuotedString, QuotedString>>,
820    pub extras: Vec<Box<dyn ToBazelDefinition>>,
821}
822
823impl RustTarget for CargoBuildScript {
824    fn rules(&self) -> Vec<Rule> {
825        vec![Rule::CargoBuildScript]
826    }
827}
828
829impl CargoBuildScript {
830    pub fn generate(
831        config: &GlobalConfig,
832        context: &CrateContext,
833        crate_config: &CrateConfig,
834        metadata: &PackageMetadata,
835    ) -> Result<Option<Self>, anyhow::Error> {
836        let crate_name = metadata.name().to_case(Case::Snake);
837        let name = QuotedString::new(format!("{crate_name}_build_script"));
838
839        // Determine the source for this build script.
840        let Some(build_script_target) = metadata.build_target(&BuildTargetId::BuildScript) else {
841            return Ok(None);
842        };
843
844        // Build scripts _should_ only ever exist at `build.rs`, but guard
845        // against them existing somewhere else.
846        let crate_root_path = metadata
847            .manifest_path()
848            .parent()
849            .ok_or_else(|| anyhow::anyhow!("package is at the root of the filesystem?"))?;
850        let script_src = build_script_target
851            .path()
852            .strip_prefix(crate_root_path)
853            .map_err(|_| anyhow::anyhow!("build script is not inside of crate"))?;
854        let script_src = Field::new(
855            "srcs",
856            List::new(vec![QuotedString::new(script_src.to_string())]),
857        );
858
859        // Collect all dependencies.
860        let all_deps = WorkspaceDependencies::new(config, metadata);
861        let (deps, extra_deps) = all_deps.iter(DependencyKind::Build, false);
862        let (proc_macro_deps, extra_proc_macro_deps) =
863            all_deps.iter(DependencyKind::Development, true);
864
865        let mut deps: List<QuotedString> =
866            List::new(deps).concat_other(AllCrateDeps::default().build());
867        // Add extra platform deps if there are any.
868        if !extra_deps.is_empty() {
869            let select: Select<List<QuotedString>> = Select::new(extra_deps, vec![]);
870            deps = deps.concat_other(select);
871        }
872
873        let mut proc_macro_deps: List<QuotedString> =
874            List::new(proc_macro_deps).concat_other(AllCrateDeps::default().build_proc_macro());
875        // Add extra platform deps if there are any.
876        if !extra_proc_macro_deps.is_empty() {
877            let select: Select<List<QuotedString>> = Select::new(extra_proc_macro_deps, vec![]);
878            proc_macro_deps = proc_macro_deps.concat_other(select);
879        }
880
881        // Generate any extra targets that we need.
882        let mut extras: Vec<Box<dyn ToBazelDefinition>> = Vec::new();
883        let mut data: List<QuotedString> = List::empty();
884
885        // Generate a filegroup for any files this build script depends on.
886        let mut protos = context
887            .build_script
888            .as_ref()
889            .map(|b| b.generated_protos.clone())
890            .unwrap_or_default();
891
892        // Make sure to add transitive dependencies.
893        let crate_filename = crate_root_path
894            .file_name()
895            .ok_or_else(|| anyhow::anyhow!("crate is at the root of the filesystem?"))?;
896        let proto_dependencies = context.build_script.as_ref().map(|b| &b.proto_dependencies);
897        if let Some(deps) = proto_dependencies {
898            let transitive_deps: BTreeSet<_> = deps
899                .iter()
900                // Google related dependencies are included via "well known types".
901                .filter(|p| !p.starts_with("google"))
902                // Imports from within the same crate are already included.
903                .filter(|p| !p.starts_with(crate_filename))
904                // This assume the root of the protobuf path is a crate, which might not be true?
905                .map(|p| p.components().next().unwrap())
906                // TODO(parkmcar): This is a bit hacky, we need to consider where
907                // we are relative to the workspace root.
908                .map(|name| format!("//src/{name}:{PROTO_FILEGROUP_NAME}"))
909                // Collect into a `BTreeSet` to de-dupe.
910                .collect();
911
912            protos.extend(transitive_deps);
913        }
914
915        if !protos.is_empty() {
916            let proto_filegroup = FileGroup::new(PROTO_FILEGROUP_NAME, protos);
917            extras.push(Box::new(proto_filegroup));
918
919            // Make sure to include this file group in the build script data!
920            data.push_back(format!(":{PROTO_FILEGROUP_NAME}"));
921        }
922
923        // Check for any metadata specified in the Cargo.toml.
924        let build_common = crate_config.build().common();
925
926        let (paths, globs) = build_common.data();
927        data.extend(paths);
928        let data = data.concat_other(globs.map(Glob::new));
929
930        let (paths, globs) = build_common.compile_data();
931        let compile_data = List::new(paths).concat_other(globs.map(Glob::new));
932
933        let rustc_flags = List::new(build_common.rustc_flags());
934        let rustc_env = Dict::new(build_common.rustc_env());
935        let build_script_env = Dict::new(crate_config.build().build_script_env());
936
937        Ok(Some(CargoBuildScript {
938            name: Field::new("name", name),
939            script_src,
940            deps: Field::new("deps", deps),
941            proc_macro_deps: Field::new("proc_macro_deps", proc_macro_deps),
942            build_script_env: Field::new("build_script_env", build_script_env),
943            data: Field::new("data", data),
944            compile_data: Field::new("compile_data", compile_data),
945            rustc_flags: Field::new("rustc_flags", rustc_flags),
946            rustc_env: Field::new("rustc_env", rustc_env),
947            extras,
948        }))
949    }
950}
951
952impl ToBazelDefinition for CargoBuildScript {
953    fn format(&self, writer: &mut dyn fmt::Write) -> Result<(), fmt::Error> {
954        let mut w = AutoIndentingWriter::new(writer);
955
956        // Write any extra targets this build script depends on.
957        for extra in &self.extras {
958            extra.format(&mut w)?;
959            writeln!(w)?;
960        }
961
962        writeln!(w, "cargo_build_script(")?;
963        {
964            let mut w = w.indent();
965
966            self.name.format(&mut w)?;
967            self.script_src.format(&mut w)?;
968            self.deps.format(&mut w)?;
969            self.proc_macro_deps.format(&mut w)?;
970            self.build_script_env.format(&mut w)?;
971
972            self.data.format(&mut w)?;
973            self.compile_data.format(&mut w)?;
974            self.rustc_flags.format(&mut w)?;
975            self.rustc_env.format(&mut w)?;
976        }
977        writeln!(w, ")")?;
978
979        Ok(())
980    }
981}
982
983/// Reads lint config from a Cargo.toml file.
984#[derive(Debug)]
985pub struct ExtractCargoLints {
986    name: Field<QuotedString>,
987    manifest: Field<QuotedString>,
988    workspace: Field<QuotedString>,
989}
990
991impl RustTarget for ExtractCargoLints {
992    fn rules(&self) -> Vec<Rule> {
993        vec![Rule::ExtractCargoLints]
994    }
995}
996
997impl ExtractCargoLints {
998    pub fn generate() -> Self {
999        ExtractCargoLints {
1000            name: Field::new("name", QuotedString::new("lints")),
1001            manifest: Field::new("manifest", QuotedString::new("Cargo.toml")),
1002            workspace: Field::new("workspace", QuotedString::new("@//:Cargo.toml")),
1003        }
1004    }
1005}
1006
1007impl ToBazelDefinition for ExtractCargoLints {
1008    fn format(&self, writer: &mut dyn fmt::Write) -> Result<(), fmt::Error> {
1009        let mut w = AutoIndentingWriter::new(writer);
1010
1011        writeln!(w, "extract_cargo_lints(")?;
1012        {
1013            let mut w = w.indent();
1014
1015            self.name.format(&mut w)?;
1016            self.manifest.format(&mut w)?;
1017            self.workspace.format(&mut w)?;
1018        }
1019        writeln!(w, ")")?;
1020
1021        Ok(())
1022    }
1023}
1024
1025/// An opaque blob of text that we treat as a target.
1026#[derive(Debug)]
1027pub struct AdditiveContent(String);
1028
1029impl AdditiveContent {
1030    pub fn new(s: &str) -> Self {
1031        AdditiveContent(s.to_string())
1032    }
1033}
1034
1035impl ToBazelDefinition for AdditiveContent {
1036    fn format(&self, writer: &mut dyn fmt::Write) -> Result<(), fmt::Error> {
1037        writeln!(writer, "{}", self.0)?;
1038        Ok(())
1039    }
1040}
1041
1042impl RustTarget for AdditiveContent {
1043    fn rules(&self) -> Vec<Rule> {
1044        vec![]
1045    }
1046}
1047
1048type AllCrateDeps = CratesUniverseMacro<AllCrateDeps_>;
1049type Aliases = CratesUniverseMacro<Aliases_>;
1050
1051/// [`crates_universe`](http://bazelbuild.github.io/rules_rust/crate_universe.html) exposes a few
1052/// macros that make it easier to define depedencies and aliases.
1053#[derive(Default, Debug)]
1054struct CratesUniverseMacro<Name> {
1055    name: Name,
1056    fields: Vec<MacroOption>,
1057}
1058
1059impl<Name> CratesUniverseMacro<Name> {
1060    pub fn normal(mut self) -> Self {
1061        self.fields.push(MacroOption::Normal);
1062        self
1063    }
1064
1065    pub fn proc_macro(mut self) -> Self {
1066        self.fields.push(MacroOption::ProcMacro);
1067        self
1068    }
1069
1070    pub fn normal_dev(mut self) -> Self {
1071        self.fields.push(MacroOption::NormalDev);
1072        self
1073    }
1074
1075    pub fn proc_macro_dev(mut self) -> Self {
1076        self.fields.push(MacroOption::ProcMacroDev);
1077        self
1078    }
1079
1080    pub fn build(mut self) -> Self {
1081        self.fields.push(MacroOption::Build);
1082        self
1083    }
1084
1085    pub fn build_proc_macro(mut self) -> Self {
1086        self.fields.push(MacroOption::BuildProcMacro);
1087        self
1088    }
1089}
1090
1091impl<N: Named> ToBazelDefinition for CratesUniverseMacro<N> {
1092    fn format(&self, writer: &mut dyn fmt::Write) -> Result<(), fmt::Error> {
1093        let mut w = AutoIndentingWriter::new(writer);
1094
1095        write!(w, "{}", self.name.name())?;
1096
1097        match &self.fields[..] {
1098            [] => write!(w, "()")?,
1099            [one] => write!(w, "({} = True)", one.to_bazel_definition())?,
1100            multiple => {
1101                write!(w, "(")?;
1102                for item in multiple {
1103                    let mut w = w.indent();
1104                    writeln!(w)?;
1105                    write!(w, "{} = True,", item.to_bazel_definition())?;
1106                }
1107                write!(w, "\n)")?;
1108            }
1109        }
1110
1111        Ok(())
1112    }
1113}
1114
1115#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)]
1116enum MacroOption {
1117    Normal,
1118    ProcMacro,
1119    NormalDev,
1120    ProcMacroDev,
1121    Build,
1122    BuildProcMacro,
1123}
1124
1125impl ToBazelDefinition for MacroOption {
1126    fn format(&self, writer: &mut dyn fmt::Write) -> Result<(), fmt::Error> {
1127        match self {
1128            MacroOption::Normal => write!(writer, "normal"),
1129            MacroOption::ProcMacro => write!(writer, "proc_macro"),
1130            MacroOption::NormalDev => write!(writer, "normal_dev"),
1131            MacroOption::ProcMacroDev => write!(writer, "proc_macro_dev"),
1132            MacroOption::Build => write!(writer, "build"),
1133            MacroOption::BuildProcMacro => write!(writer, "build_proc_macro"),
1134        }
1135    }
1136}
1137
1138/// A hack for const generic strings.
1139trait Named: std::fmt::Debug {
1140    fn name(&self) -> &'static str;
1141}
1142#[derive(Default, Debug)]
1143struct AllCrateDeps_;
1144impl Named for AllCrateDeps_ {
1145    fn name(&self) -> &'static str {
1146        "all_crate_deps"
1147    }
1148}
1149#[derive(Default, Debug)]
1150struct Aliases_;
1151impl Named for Aliases_ {
1152    fn name(&self) -> &'static str {
1153        "aliases"
1154    }
1155}
1156
1157struct WorkspaceDependencies<'a> {
1158    config: &'a GlobalConfig,
1159    package: &'a PackageMetadata<'a>,
1160}
1161
1162impl<'a> WorkspaceDependencies<'a> {
1163    pub fn new(config: &'a GlobalConfig, package: &'a PackageMetadata<'a>) -> Self {
1164        WorkspaceDependencies { config, package }
1165    }
1166
1167    /// Returns a set of dependencies that are common to all platforms, and
1168    /// then any additional dependencies that need to be enabled for a specific
1169    /// platform.
1170    pub fn iter(
1171        &self,
1172        kind: DependencyKind,
1173        proc_macro: bool,
1174    ) -> (BTreeSet<String>, BTreeMap<PlatformVariant, Vec<String>>) {
1175        let feature_set = platform_feature_sets(self.package);
1176
1177        let dependencies: BTreeMap<_, _> = feature_set
1178            .into_iter()
1179            .map(|(platform, feature_set)| {
1180                // Convert the feature set to the set of packages it enables.
1181                let deps: BTreeSet<_> = feature_set
1182                    .to_package_set()
1183                    .links(DependencyDirection::Reverse)
1184                    // Filter down to only direct dependencies.
1185                    .filter(|link| link.from().id() == self.package.id())
1186                    .filter(move |link| match kind {
1187                        DependencyKind::Build | DependencyKind::Normal => {
1188                            link.req_for_kind(kind).is_present()
1189                        }
1190                        // Tests can rely on normal dependencies, so also check those.
1191                        DependencyKind::Development => {
1192                            link.req_for_kind(kind).is_present()
1193                                || link.req_for_kind(DependencyKind::Normal).is_present()
1194                        }
1195                    })
1196                    .map(|link| link.to())
1197                    // Ignore deps filtered out by the global config.
1198                    .filter(|meta| self.config.include_dep(meta.name()))
1199                    // Filter proc_macro deps.
1200                    .filter(move |meta| meta.is_proc_macro() == proc_macro)
1201                    // Filter map down to only deps in the workspace, and their path.
1202                    .filter_map(|meta| meta.source().workspace_path().map(|p| (p, meta)))
1203                    .map(|(path, meta)| {
1204                        let crate_name = meta.name().to_case(Case::Snake);
1205                        format!("//{}:{}", path, crate_name)
1206                    })
1207                    .collect();
1208
1209                (platform, deps)
1210            })
1211            .collect();
1212
1213        // Dependencies that are common to all platforms.
1214        let common = dependencies
1215            .iter()
1216            .fold(None, |common, (_variant, set)| match common {
1217                None => Some(set.clone()),
1218                Some(common) => Some(common.intersection(set).cloned().collect()),
1219            })
1220            .unwrap_or_default();
1221
1222        // Extra features for each platform that need to be enabled.
1223        let extras: BTreeMap<_, _> = dependencies
1224            .into_iter()
1225            .filter_map(|(variant, features)| {
1226                let extra: Vec<_> = features.difference(&common).cloned().collect();
1227                if extra.is_empty() {
1228                    None
1229                } else {
1230                    Some((variant, extra))
1231                }
1232            })
1233            .collect();
1234
1235        (common, extras)
1236    }
1237}
1238
1239/// Returns a set of Cargo features that are common to all platforms, and then
1240/// any additional features that need to be enabled for a specific platform.
1241pub fn crate_features<'a>(
1242    config: &'a GlobalConfig,
1243    package: &'a PackageMetadata<'a>,
1244) -> Result<(BTreeSet<String>, BTreeMap<PlatformVariant, Vec<String>>), anyhow::Error> {
1245    // Resolve feature sets for all of the platforms we care about.
1246    let feature_sets = platform_feature_sets(package);
1247
1248    // Filter down to just the feature labels for this crate.
1249    let features: BTreeMap<_, _> = feature_sets
1250        .into_iter()
1251        .map(|(platform, feature_set)| {
1252            // Filter down to the features for just this crate.
1253            let features: BTreeSet<_> = feature_set
1254                .features_for(package.id())
1255                .expect("package id should be known")
1256                .expect("package id should be in the feature set")
1257                .into_iter()
1258                .filter_map(|feature| match feature.label() {
1259                    FeatureLabel::Base => None,
1260                    FeatureLabel::Named(f) => Some(f),
1261                    FeatureLabel::OptionalDependency(f) => Some(f),
1262                })
1263                // TODO(parkmycar): We shouldn't ignore features based on name, but if
1264                // enabling that feature would result in us depending on a crate we
1265                // want to ignore.
1266                .filter(|name| config.include_dep(name))
1267                .map(|s| s.to_string())
1268                .collect();
1269
1270            (platform, features)
1271        })
1272        .collect();
1273
1274    // Features that are common to all platforms.
1275    let common = features
1276        .iter()
1277        .fold(None, |common, (_variant, set)| match common {
1278            None => Some(set.clone()),
1279            Some(common) => Some(common.intersection(set).cloned().collect()),
1280        })
1281        .unwrap_or_default();
1282
1283    // Extra features for each platform that need to be enabled.
1284    let extras: BTreeMap<_, _> = features
1285        .into_iter()
1286        .filter_map(|(variant, features)| {
1287            let extra: Vec<_> = features.difference(&common).cloned().collect();
1288            if extra.is_empty() {
1289                None
1290            } else {
1291                Some((variant, extra))
1292            }
1293        })
1294        .collect();
1295
1296    Ok((common, extras))
1297}
1298
1299/// Returns a [`FeatureSet`] of reverse dependencies (in other words, all
1300/// crates that depend on) for the provided package, for every platform that we
1301/// support.
1302///
1303/// TODO(parkmycar): Make the list of platforms configurable.
1304pub fn platform_feature_sets<'a>(
1305    package: &'a PackageMetadata<'a>,
1306) -> BTreeMap<PlatformVariant, FeatureSet<'a>> {
1307    // Resolve a feature graph for all crates that depend on this one.
1308    let dependents = package
1309        .to_package_query(DependencyDirection::Reverse)
1310        .resolve()
1311        .to_feature_set(StandardFeatures::Default);
1312
1313    PlatformVariant::all()
1314        .iter()
1315        .map(|p| {
1316            // Resolve all features enabled for the specified platform.
1317            let feature_set = dependents
1318                .to_feature_query(DependencyDirection::Forward)
1319                .resolve_with_fn(|_query, cond_link| {
1320                    // Note: We don't currently generate different targets for
1321                    // dev or build dependencies, but possibly could if need be.
1322                    let normal_enabled = cond_link.normal().enabled_on(p.spec());
1323                    let dev_enabled = cond_link.dev().enabled_on(p.spec());
1324                    let build_enabled = cond_link.build().enabled_on(p.spec());
1325
1326                    matches!(normal_enabled, EnabledTernary::Enabled)
1327                        || matches!(dev_enabled, EnabledTernary::Enabled)
1328                        || matches!(build_enabled, EnabledTernary::Enabled)
1329                });
1330
1331            (*p, feature_set)
1332        })
1333        .collect()
1334}