cargo_gazelle/
lib.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
10use std::collections::{BTreeMap, VecDeque};
11use std::fmt::{self, Debug, Write};
12use std::rc::Rc;
13
14use crate::targets::RustTarget;
15
16pub mod args;
17pub mod config;
18pub mod context;
19pub mod header;
20pub mod platforms;
21pub mod rules;
22pub mod targets;
23
24/// An entire `BUILD.bazel` file.
25///
26/// This includes an auto-generated header, `load(...)` statements, and all
27/// Bazel targets.
28pub struct BazelBuildFile {
29    pub header: header::BazelHeader,
30    pub targets: Vec<Box<dyn RustTarget>>,
31}
32
33impl fmt::Display for BazelBuildFile {
34    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
35        self.header.format(f)?;
36        for target in &self.targets {
37            writeln!(f)?;
38            target.format(f)?;
39        }
40        Ok(())
41    }
42}
43
44/// Formatting trait for converting a type to its `BUILD.bazel` representation.
45pub trait ToBazelDefinition: Debug {
46    fn format(&self, writer: &mut dyn fmt::Write) -> Result<(), fmt::Error>;
47
48    fn to_bazel_definition(&self) -> String {
49        let mut buf = String::new();
50        self.format(&mut buf).expect("failed to write into string");
51        buf
52    }
53}
54
55impl<T: ToBazelDefinition> ToBazelDefinition for Option<T> {
56    fn format(&self, writer: &mut dyn fmt::Write) -> Result<(), fmt::Error> {
57        match self {
58            Some(val) => val.format(writer),
59            None => Ok(()),
60        }
61    }
62}
63
64impl ToBazelDefinition for bool {
65    fn format(&self, writer: &mut dyn fmt::Write) -> Result<(), fmt::Error> {
66        let bazel_str = if *self { "True" } else { "False" };
67        writer.write_str(bazel_str)
68    }
69}
70
71/// Wrapper around a [`std::fmt::Write`] that helps write at the correct level of indentation.
72struct AutoIndentingWriter<'w> {
73    level: usize,
74    should_indent: bool,
75    writer: &'w mut dyn fmt::Write,
76}
77
78impl<'w> AutoIndentingWriter<'w> {
79    fn new(writer: &'w mut dyn fmt::Write) -> Self {
80        AutoIndentingWriter {
81            level: 0,
82            should_indent: true,
83            writer,
84        }
85    }
86
87    fn indent(&mut self) -> AutoIndentingWriter<'_> {
88        AutoIndentingWriter {
89            level: self.level + 1,
90            should_indent: self.should_indent,
91            writer: self.writer,
92        }
93    }
94}
95
96impl<'w> fmt::Write for AutoIndentingWriter<'w> {
97    fn write_str(&mut self, s: &str) -> fmt::Result {
98        let lines = s.split_inclusive('\n');
99
100        for line in lines {
101            if self.should_indent {
102                for _ in 0..self.level {
103                    self.writer.write_char('\t')?;
104                }
105            }
106
107            self.writer.write_str(line)?;
108            self.should_indent = line.ends_with('\n');
109        }
110
111        Ok(())
112    }
113}
114
115/// A [`String`] that when formatted for Bazel is quoted.
116///
117/// ```
118/// use cargo_gazelle::{QuotedString, ToBazelDefinition};
119///
120/// let deps = QuotedString::new("json");
121/// assert_eq!(deps.to_bazel_definition(), "\"json\"");
122/// ```
123#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
124pub struct QuotedString(String);
125
126impl QuotedString {
127    pub fn new(s: impl Into<String>) -> Self {
128        QuotedString(s.into())
129    }
130
131    /// Returns the inner value of the string, unquoted.
132    pub fn unquoted(&self) -> &str {
133        &self.0
134    }
135}
136
137impl ToBazelDefinition for QuotedString {
138    fn format(&self, writer: &mut dyn Write) -> Result<(), fmt::Error> {
139        write!(writer, "\"{}\"", self.0)?;
140        Ok(())
141    }
142}
143
144impl<T: ToString> From<T> for QuotedString {
145    fn from(value: T) -> Self {
146        QuotedString(value.to_string())
147    }
148}
149
150/// A field within a Build rule, e.g. `name = "foo"`.
151///
152/// ```
153/// use cargo_gazelle::{Field, List, QuotedString, ToBazelDefinition};
154///
155/// let deps: Field<List<QuotedString>> = Field::new(
156///     "crate_features",
157///     List::new(vec![QuotedString::new("json")],
158/// ));
159/// assert_eq!(deps.to_bazel_definition(), "crate_features = [\"json\"],\n");
160/// ```
161#[derive(Debug, Clone)]
162pub struct Field<T> {
163    name: String,
164    value: T,
165}
166
167impl<T> Field<T> {
168    pub fn new(name: impl Into<String>, value: T) -> Self {
169        Field {
170            name: name.into(),
171            value,
172        }
173    }
174
175    pub fn name(&self) -> &str {
176        &self.name
177    }
178}
179
180impl<T: ToBazelDefinition> ToBazelDefinition for Field<T> {
181    fn format(&self, writer: &mut dyn fmt::Write) -> Result<(), fmt::Error> {
182        write!(writer, "{} = ", self.name)?;
183        self.value.format(writer)?;
184        writeln!(writer, ",")?;
185
186        Ok(())
187    }
188}
189
190/// Helper for formatting a list of items.
191///
192/// ```
193/// use cargo_gazelle::{List, QuotedString, ToBazelDefinition};
194///
195/// let deps: List<QuotedString> = List::new(vec![QuotedString::new("tokio")]);
196/// assert_eq!(deps.to_bazel_definition(), "[\"tokio\"]");
197/// ```
198#[derive(Debug, Clone)]
199pub struct List<T> {
200    items: Vec<T>,
201    objects: Vec<Rc<dyn ToBazelDefinition>>,
202}
203
204impl<T> List<T> {
205    pub fn new<E: Into<T>, I: IntoIterator<Item = E>>(items: I) -> Self {
206        List {
207            items: items.into_iter().map(Into::into).collect(),
208            objects: Vec::new(),
209        }
210    }
211
212    pub fn empty() -> Self {
213        List::new(VecDeque::<T>::new())
214    }
215
216    /// Concatenate another Bazel object to this list.
217    ///
218    /// Concretely this will result in a generated Bazel list like `[ ... ] + <concat>`.
219    ///
220    /// TODO(parkmcar): This feels a bit off, maybe the API should be something like
221    /// `LinkedList`?
222    pub fn concat_other(mut self, other: impl ToBazelDefinition + 'static) -> Self {
223        self.objects.push(Rc::new(other));
224        self
225    }
226
227    /// Push a value of `T` to the front of the list.
228    pub fn push_front<E: Into<T>>(&mut self, val: E) {
229        self.items.insert(0, val.into())
230    }
231
232    /// Push a value of `T` to the back of the list.
233    pub fn push_back<E: Into<T>>(&mut self, val: E) {
234        self.items.push(val.into())
235    }
236
237    /// Extend `self` with the values from `vals`.
238    pub fn extend<E: Into<T>, I: IntoIterator<Item = E>>(&mut self, vals: I) {
239        self.items.extend(vals.into_iter().map(Into::into))
240    }
241
242    /// Returns an iterator over all of the `items`.
243    pub fn iter(&self) -> impl Iterator<Item = &T> {
244        self.items.iter()
245    }
246
247    /// Returns if this [`List`] is empty.
248    pub fn is_empty(&self) -> bool {
249        self.items.is_empty() && self.objects.is_empty()
250    }
251}
252
253impl<A, B, T> From<T> for List<A>
254where
255    B: Into<A>,
256    T: IntoIterator<Item = B>,
257{
258    fn from(value: T) -> Self {
259        List::from_iter(value.into_iter().map(|x| x.into()))
260    }
261}
262
263impl<A> FromIterator<A> for List<A> {
264    fn from_iter<T: IntoIterator<Item = A>>(iter: T) -> Self {
265        List {
266            items: iter.into_iter().collect(),
267            objects: Vec::new(),
268        }
269    }
270}
271
272impl<T: ToBazelDefinition> ToBazelDefinition for List<T> {
273    fn format(&self, writer: &mut dyn fmt::Write) -> Result<(), fmt::Error> {
274        let mut w = AutoIndentingWriter::new(writer);
275
276        match &self.items[..] {
277            [] => write!(w, "[]")?,
278            [one] => write!(w, "[{}]", one.to_bazel_definition())?,
279            multiple => {
280                write!(w, "[")?;
281                for item in multiple {
282                    let mut w = w.indent();
283                    writeln!(w)?;
284                    write!(w, "{},", item.to_bazel_definition())?;
285                }
286                write!(w, "\n]")?;
287            }
288        }
289
290        for o in &self.objects {
291            let def = o.to_bazel_definition();
292            if !def.is_empty() {
293                write!(w, " + {def}")?;
294            }
295        }
296
297        Ok(())
298    }
299}
300
301/// Helper for formatting a dictionary.
302///
303/// ```
304/// use cargo_gazelle::{Dict, QuotedString, ToBazelDefinition};
305///
306/// let entry = (QuotedString::new("RUST_LOG"), QuotedString::new("INFO"));
307/// let deps: Dict<QuotedString, QuotedString> = Dict::new(vec![entry]);
308/// assert_eq!(deps.to_bazel_definition(), "{ \"RUST_LOG\": \"INFO\" }");
309/// ```
310#[derive(Debug)]
311pub struct Dict<K, V> {
312    items: BTreeMap<K, V>,
313}
314
315impl<K, V> Dict<K, V> {
316    pub fn new<M, N, I>(vals: I) -> Self
317    where
318        M: Into<K>,
319        N: Into<V>,
320        I: IntoIterator<Item = (M, N)>,
321        K: Ord,
322    {
323        Dict {
324            items: vals
325                .into_iter()
326                .map(|(m, n)| (m.into(), n.into()))
327                .collect(),
328        }
329    }
330
331    pub fn empty() -> Self {
332        Dict {
333            items: BTreeMap::default(),
334        }
335    }
336
337    pub fn insert<M, N>(&mut self, key: M, val: N)
338    where
339        M: Into<K>,
340        N: Into<V>,
341        K: Ord,
342    {
343        self.items.insert(key.into(), val.into());
344    }
345
346    pub fn extend<M, N, I>(&mut self, vals: I)
347    where
348        M: Into<K>,
349        N: Into<V>,
350        I: IntoIterator<Item = (M, N)>,
351        K: Ord,
352    {
353        self.items
354            .extend(vals.into_iter().map(|(k, v)| (k.into(), v.into())));
355    }
356}
357
358impl<K: ToBazelDefinition, V: ToBazelDefinition> ToBazelDefinition for Dict<K, V> {
359    fn format(&self, writer: &mut dyn fmt::Write) -> Result<(), fmt::Error> {
360        let mut w = AutoIndentingWriter::new(writer);
361
362        match self.items.len() {
363            0 => write!(w, "{{}}")?,
364            1 => {
365                let (key, val) = self.items.iter().next().expect("checked length");
366                write!(
367                    w,
368                    "{{ {}: {} }}",
369                    key.to_bazel_definition(),
370                    val.to_bazel_definition()
371                )?;
372            }
373            _ => {
374                write!(w, "{{")?;
375                for (key, val) in &self.items {
376                    let mut w = w.indent();
377                    writeln!(w)?;
378                    write!(
379                        w,
380                        "{{ {}: {} }}",
381                        key.to_bazel_definition(),
382                        val.to_bazel_definition()
383                    )?;
384                }
385                write!(w, "\n}}")?;
386            }
387        }
388
389        Ok(())
390    }
391}
392
393/// A Bazel [`filegroup`](https://bazel.build/reference/be/general#filegroup).
394#[derive(Debug)]
395pub struct FileGroup {
396    name: Field<QuotedString>,
397    files: Field<List<QuotedString>>,
398}
399
400impl FileGroup {
401    pub fn new<S: Into<String>>(
402        name: impl Into<String>,
403        files: impl IntoIterator<Item = S>,
404    ) -> Self {
405        let name = Field::new("name", QuotedString::new(name.into()));
406        let files = Field::new(
407            "srcs",
408            files.into_iter().map(|f| QuotedString::new(f)).collect(),
409        );
410
411        FileGroup { name, files }
412    }
413}
414
415impl ToBazelDefinition for FileGroup {
416    fn format(&self, writer: &mut dyn fmt::Write) -> Result<(), fmt::Error> {
417        let mut w = AutoIndentingWriter::new(writer);
418
419        writeln!(w, "filegroup(")?;
420        {
421            let mut w = w.indent();
422            self.name.format(&mut w)?;
423            self.files.format(&mut w)?;
424        }
425        writeln!(w, ")")?;
426
427        Ok(())
428    }
429}
430
431/// A Bazel [`alias`](https://bazel.build/reference/be/general#alias)
432#[derive(Debug)]
433pub struct Alias {
434    name: Field<QuotedString>,
435    actual: Field<QuotedString>,
436}
437
438impl Alias {
439    pub fn new<N: Into<String>, A: Into<String>>(name: N, actual: A) -> Self {
440        let name = Field::new("name", QuotedString::new(name.into()));
441        let actual = Field::new("actual", QuotedString::new(actual.into()));
442
443        Alias { name, actual }
444    }
445}
446
447impl ToBazelDefinition for Alias {
448    fn format(&self, writer: &mut dyn fmt::Write) -> Result<(), fmt::Error> {
449        let mut w = AutoIndentingWriter::new(writer);
450
451        writeln!(w, "alias(")?;
452        {
453            let mut w = w.indent();
454            self.name.format(&mut w)?;
455            self.actual.format(&mut w)?;
456        }
457        writeln!(w, ")")?;
458
459        Ok(())
460    }
461}
462
463/// A Bazel [`glob`](https://bazel.build/reference/be/functions#glob)
464///
465/// TODO(parkmcar): Support `excludes`.
466#[derive(Debug)]
467pub struct Glob {
468    includes: List<QuotedString>,
469}
470
471impl Glob {
472    pub fn new<E, I>(globs: I) -> Glob
473    where
474        E: Into<QuotedString>,
475        I: IntoIterator<Item = E>,
476    {
477        Glob {
478            includes: List::new(globs),
479        }
480    }
481}
482
483impl ToBazelDefinition for Glob {
484    fn format(&self, writer: &mut dyn fmt::Write) -> Result<(), fmt::Error> {
485        write!(writer, "glob(")?;
486        self.includes.format(writer)?;
487        write!(writer, ")")?;
488
489        Ok(())
490    }
491}
492
493/// Helper for formatting a [`select({ ... })`](https://bazel.build/reference/be/functions#select) function.
494///
495/// ```
496/// use cargo_gazelle::{
497///     List,
498///     QuotedString,
499///     Select,
500///     ToBazelDefinition,
501/// };
502/// use cargo_gazelle::platforms::PlatformVariant;
503///
504/// let features = List::new(["json"]);
505/// let features = [(PlatformVariant::Aarch64MacOS, features)];
506///
507/// let select: Select<List<QuotedString>> = Select::new(features, List::empty());
508/// assert_eq!(select.to_bazel_definition(), "select({\n\t\"@//misc/bazel/platforms:macos_arm\": [\"json\"],\n\t\"//conditions:default\": [],\n})");
509/// ```
510#[derive(Debug)]
511pub struct Select<T> {
512    entries: BTreeMap<String, T>,
513    default: T,
514}
515
516impl<T> Select<T> {
517    pub fn new<E, I, J>(entires: I, default: E) -> Select<T>
518    where
519        E: Into<T>,
520        J: ToBazelDefinition,
521        I: IntoIterator<Item = (J, E)>,
522    {
523        Select {
524            entries: entires
525                .into_iter()
526                .map(|(variant, entry)| (variant.to_bazel_definition(), entry.into()))
527                .collect(),
528            default: default.into(),
529        }
530    }
531}
532
533impl<T: ToBazelDefinition> ToBazelDefinition for Select<T> {
534    fn format(&self, writer: &mut dyn fmt::Write) -> Result<(), fmt::Error> {
535        let mut w = AutoIndentingWriter::new(writer);
536
537        writeln!(w, "select({{")?;
538        {
539            let mut w = w.indent();
540            for (variant, entry) in &self.entries {
541                write!(w, "{variant}")?;
542                write!(w, ": ")?;
543                entry.format(&mut w)?;
544                writeln!(w, ",")?;
545            }
546
547            write!(w, "\"//conditions:default\": ")?;
548            self.default.format(&mut w)?;
549            writeln!(w, ",")?;
550        }
551        write!(w, "}})")?;
552
553        Ok(())
554    }
555}