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