Skip to main content

dev_prune/adapters/
vcpkg.rs

1// Copyright 2026 VKrishna04
2// SPDX-License-Identifier: Apache-2.0
3
4// vcpkg adapter, for C and C++ projects in manifest mode.
5//
6// Opt-in, for the same reason as Cargo, Gradle, Maven and SwiftPM: `vcpkg_installed/`
7// is not a downloaded dependency tree. vcpkg builds every port from source, so what is
8// in there is headers and compiled libraries, and `vcpkg install` puts them back by
9// compiling them again — Boost or Qt is an afternoon, not a download. The binary cache
10// beside the vcpkg installation often turns that back into a copy, but nothing here can
11// prove it holds an archive matching this project's triplet and ABI, so the adapter
12// assumes the expensive answer and the engine holds it to `build_idle_days`.
13//
14// Manifest mode only, which is the mode that puts anything inside a repository at all:
15// classic mode installs into one tree beside vcpkg itself, shared by every project on
16// the machine. `devp caches` reports that one instead.
17
18use super::{BloatDir, EnforcePolicy, PackageManager, dir_size};
19use anyhow::{Result, anyhow};
20use std::fs;
21use std::path::Path;
22
23/// vcpkg adapter. Opt-in; see the module comment.
24pub struct Vcpkg;
25
26impl PackageManager for Vcpkg {
27    fn name(&self) -> &'static str {
28        "vcpkg"
29    }
30
31    fn detect(&self, path: &Path) -> bool {
32        path.join("vcpkg.json").exists()
33    }
34
35    /// `vcpkg_installed/`, which vcpkg creates beside the manifest it read. `build/` next
36    /// to it belongs to CMake, and the name alone never says whose it is.
37    fn bloat_dirs(&self, path: &Path) -> Vec<BloatDir> {
38        let installed = path.join("vcpkg_installed");
39        if !installed.is_dir() {
40            return Vec::new();
41        }
42        vec![BloatDir {
43            name: "vcpkg_installed".to_string(),
44            path: installed.clone(),
45            size_bytes: dir_size(&installed),
46            shared_bytes: 0,
47        }]
48    }
49
50    /// The manifest is the proof, as it is for Maven and SwiftPM — with one extra
51    /// condition. `vcpkg.json` is also the file every *port* carries, and a port manifest
52    /// describes a package rather than an installation: nothing rebuilds a
53    /// `vcpkg_installed/` from it. What separates the two is a `dependencies` list, so
54    /// that is what is checked. A manifest declaring nothing to install cannot account
55    /// for the directory beside it either, which is the same refusal for a different
56    /// reason.
57    ///
58    /// Running `vcpkg install --dry-run` here instead would need a registry checkout and
59    /// a network fetch in the middle of a delete pass, for no stronger answer than "the
60    /// file that rebuilds this is present and names dependencies".
61    fn enforce_lockfile(&self, path: &Path, _policy: EnforcePolicy) -> Result<()> {
62        let manifest = path.join("vcpkg.json");
63        let raw = fs::read_to_string(&manifest).map_err(|e| {
64            anyhow!(
65                "`vcpkg.json` could not be read ({e}) — nothing to rebuild `vcpkg_installed/` from."
66            )
67        })?;
68        let json: serde_json::Value = serde_json::from_str(&raw).map_err(|e| {
69            anyhow!(
70                "`vcpkg.json` is not valid JSON ({e}) — `vcpkg install` could not read it either."
71            )
72        })?;
73        let declares_dependencies = json
74            .get("dependencies")
75            .and_then(|d| d.as_array())
76            .is_some_and(|d| !d.is_empty());
77        if !declares_dependencies {
78            return Err(anyhow!(
79                "`vcpkg.json` declares no `dependencies` — refusing to treat \
80                 `vcpkg_installed/` as rebuildable from it."
81            ));
82        }
83        Ok(())
84    }
85
86    fn restore(&self, _path: &Path, _timeout: std::time::Duration) -> Result<()> {
87        println!("vcpkg vcpkg_installed/ will regenerate on the next `vcpkg install`");
88        Ok(())
89    }
90
91    fn lockfiles(&self) -> &'static [&'static str] {
92        &["vcpkg.json"]
93    }
94
95    fn opt_in(&self) -> bool {
96        true
97    }
98}
99
100#[cfg(test)]
101mod tests {
102    use super::*;
103    use tempfile::tempdir;
104
105    fn manifest(dir: &Path, body: &str) {
106        fs::write(dir.join("vcpkg.json"), body).unwrap();
107    }
108
109    fn enforced(dir: &Path) -> Result<()> {
110        Vcpkg.enforce_lockfile(dir, EnforcePolicy::default())
111    }
112
113    #[test]
114    fn detects_on_the_manifest() {
115        let dir = tempdir().unwrap();
116        assert!(!Vcpkg.detect(dir.path()));
117        manifest(dir.path(), r#"{"dependencies":["fmt"]}"#);
118        assert!(Vcpkg.detect(dir.path()));
119    }
120
121    #[test]
122    fn claims_the_manifest_install_tree_and_nothing_else() {
123        // `build/` beside a vcpkg manifest is CMake's, and a directory named `build` is
124        // as often a hand-written one as a generated one. Claiming it on the name would
125        // be the guess this project does not make.
126        let dir = tempdir().unwrap();
127        fs::create_dir(dir.path().join("vcpkg_installed")).unwrap();
128        fs::create_dir(dir.path().join("build")).unwrap();
129        let names: Vec<String> = Vcpkg
130            .bloat_dirs(dir.path())
131            .into_iter()
132            .map(|b| b.name)
133            .collect();
134        assert_eq!(names, vec!["vcpkg_installed"]);
135    }
136
137    #[test]
138    fn a_port_manifest_is_not_an_installation() {
139        // Every vcpkg *port* carries a `vcpkg.json` too. It describes a package rather
140        // than an install root, so nothing rebuilds a `vcpkg_installed/` from it and a
141        // port directory that happens to hold one must not be pruned under it.
142        let dir = tempdir().unwrap();
143        manifest(dir.path(), r#"{"name":"fmt","version":"10.1.1"}"#);
144        assert!(enforced(dir.path()).is_err());
145    }
146
147    #[test]
148    fn a_missing_empty_or_unreadable_manifest_is_refused() {
149        let dir = tempdir().unwrap();
150        assert!(enforced(dir.path()).is_err(), "no manifest at all");
151        manifest(dir.path(), "{ not json");
152        assert!(enforced(dir.path()).is_err(), "unparseable manifest");
153        manifest(dir.path(), r#"{"dependencies":[]}"#);
154        assert!(
155            enforced(dir.path()).is_err(),
156            "an empty dependency list rebuilds nothing"
157        );
158    }
159
160    #[test]
161    fn a_manifest_with_dependencies_is_the_proof() {
162        let dir = tempdir().unwrap();
163        manifest(
164            dir.path(),
165            r#"{"dependencies":["fmt","zlib"],
166                "builtin-baseline":"3426db05b996481ca31e95fff3734cf23e0f51bc"}"#,
167        );
168        assert!(enforced(dir.path()).is_ok());
169
170        // The object form is what a dependency with features or a host requirement looks
171        // like, and it is commoner than the bare string in manifests that pull anything
172        // substantial.
173        manifest(
174            dir.path(),
175            r#"{"dependencies":[{"name":"boost-asio","features":["ssl"]}]}"#,
176        );
177        assert!(enforced(dir.path()).is_ok());
178    }
179
180    #[test]
181    fn vcpkg_is_opt_in() {
182        assert!(Vcpkg.opt_in());
183    }
184}