Skip to main content

dev_prune/adapters/
mix_build.rs

1// Copyright 2026 VKrishna04
2// SPDX-License-Identifier: Apache-2.0
3
4// Mix build-tree adapter for Elixir and Erlang projects.
5//
6// Opt-in (`devp config set enable_mix_build true`), and separate from the `mix` adapter
7// on purpose: `deps/` is downloaded and `_build/` is compiled. The plain adapter can
8// delete `deps/` with `_build/` still in place because Mix refetches the sources and
9// recompiles only what changed. Deleting `_build/` itself costs a full recompile of the
10// project *and* every dependency, which on a Phoenix application is minutes — so it
11// waits for someone to ask for it, and then for the longer `build_idle_days` window on
12// top of that.
13//
14// What it claims is `_build/` and nothing else. Hex's package cache lives in
15// `~/.hex/packages`, outside the repository, and is `devp caches` business rather than
16// this adapter's.
17
18use super::{BloatDir, EnforcePolicy, PackageManager, dir_size};
19use anyhow::{Result, anyhow};
20use std::path::Path;
21
22/// Adapter for the Mix build tree. Opt-in; see the module comment.
23pub struct MixBuild;
24
25impl PackageManager for MixBuild {
26    fn name(&self) -> &'static str {
27        "mix_build"
28    }
29
30    fn detect(&self, path: &Path) -> bool {
31        path.join("mix.exs").exists()
32    }
33
34    fn bloat_dirs(&self, path: &Path) -> Vec<BloatDir> {
35        let build = path.join("_build");
36        if !build.is_dir() {
37            return Vec::new();
38        }
39        vec![BloatDir {
40            name: "_build".to_string(),
41            path: build.clone(),
42            size_bytes: dir_size(&build),
43            shared_bytes: 0,
44        }]
45    }
46
47    /// Like Gradle and Maven: the rebuild starts from the sources and `mix.exs` in the
48    /// tree, so their presence is the recoverability proof. `mix.lock` is checked too —
49    /// a recompile of the dependencies needs the same versions back, and that is the
50    /// file that pins them.
51    fn enforce_lockfile(&self, path: &Path, _policy: EnforcePolicy) -> Result<()> {
52        if !path.join("mix.exs").exists() {
53            return Err(anyhow!("no `mix.exs` — nothing to rebuild `_build/` from."));
54        }
55        if !path.join("mix.lock").exists() {
56            return Err(anyhow!(
57                "`mix.lock` is missing — recompiling `_build/` needs the dependency \
58                 versions it pins, and without it `mix deps.get` resolves afresh."
59            ));
60        }
61        Ok(())
62    }
63
64    fn restore(&self, _path: &Path, _timeout: std::time::Duration) -> Result<()> {
65        println!("Mix _build/ will regenerate on the next `mix compile`");
66        Ok(())
67    }
68
69    fn lockfiles(&self) -> &'static [&'static str] {
70        &["mix.lock"]
71    }
72
73    fn opt_in(&self) -> bool {
74        true
75    }
76}
77
78#[cfg(test)]
79mod tests {
80    use super::*;
81    use std::fs;
82    use tempfile::tempdir;
83
84    #[test]
85    fn detects_on_the_mix_manifest() {
86        let dir = tempdir().unwrap();
87        assert!(!MixBuild.detect(dir.path()));
88        fs::write(dir.path().join("mix.exs"), "defmodule X.MixProject do end").unwrap();
89        assert!(MixBuild.detect(dir.path()));
90    }
91
92    #[test]
93    fn claims_the_build_tree_and_never_deps() {
94        let dir = tempdir().unwrap();
95        fs::create_dir(dir.path().join("deps")).unwrap();
96        fs::create_dir(dir.path().join("_build")).unwrap();
97        let names: Vec<String> = MixBuild
98            .bloat_dirs(dir.path())
99            .into_iter()
100            .map(|b| b.name)
101            .collect();
102        assert_eq!(names, vec!["_build"]);
103    }
104
105    #[test]
106    fn a_missing_manifest_or_lockfile_is_refused() {
107        let dir = tempdir().unwrap();
108        assert!(
109            MixBuild
110                .enforce_lockfile(dir.path(), EnforcePolicy::default())
111                .is_err()
112        );
113        fs::write(dir.path().join("mix.exs"), "").unwrap();
114        assert!(
115            MixBuild
116                .enforce_lockfile(dir.path(), EnforcePolicy::default())
117                .is_err()
118        );
119        fs::write(dir.path().join("mix.lock"), "%{}\n").unwrap();
120        assert!(
121            MixBuild
122                .enforce_lockfile(dir.path(), EnforcePolicy::default())
123                .is_ok()
124        );
125    }
126
127    #[test]
128    fn the_build_tree_is_opt_in() {
129        assert!(MixBuild.opt_in());
130        assert!(!crate::adapters::mix::Mix.opt_in());
131    }
132}