Skip to content

Commit df19074

Browse files
vmagrometa-codesync[bot]
authored andcommitted
[antlir2][debian] snapshot metadata tree
Summary: Snapshot a cheaply-produced tree of metadata artifacts into Manifold. This is not yet wired up to anything that can persist the output into something usable in a Buck target - that will follow. Test Plan: ``` ❯ buck2 bxl fbcode//mode/opt fbcode//antlir/antlir2/package_managers/snapshot/snapshot.bxl:main -- --target fbcode//antlir/antlir2/package_managers/deb:trixie --storage {"type":"manifold", "bucket": "antlir_snapshots", "api_key": "antlir_snapshots-key", "path_prefix": "tree/deb/snap-2026-02-27+16:06:14"} buck-out/v2/art-bxl/fbcode/86037fb0bf2cbdca/antlir/antlir2/package_managers/snapshot/snapshot.bxl/__main__/34f11e79e91b530d/metadata.json ❯ jq < buck-out/v2/art-bxl/fbcode/86037fb0bf2cbdca/antlir/antlir2/package_managers/snapshot/snapshot.bxl/__main__/34f11e79e91b530d/metadata.json { "files": { "archive/dists/trixie/InRelease": "mf://antlir_snapshots-key@antlir_snapshots/tree/tree/deb/snap-2026-02-27+16:06:14/archive/dists/trixie/InRelease", "archive/dists/trixie/contrib/binary-amd64/Packages": "mf://antlir_snapshots-key@antlir_snapshots/tree/tree/deb/snap-2026-02-27+16:06:14/archive/dists/trixie/contrib/binary-amd64/Packages", "archive/dists/trixie/main/binary-amd64/Packages": "mf://antlir_snapshots-key@antlir_snapshots/tree/tree/deb/snap-2026-02-27+16:06:14/archive/dists/trixie/main/binary-amd64/Packages", "components/contrib/packages.json": "mf://antlir_snapshots-key@antlir_snapshots/tree/tree/deb/snap-2026-02-27+16:06:14/components/contrib/packages.json", "components/main/packages.json": "mf://antlir_snapshots-key@antlir_snapshots/tree/tree/deb/snap-2026-02-27+16:06:14/components/main/packages.json", "release.json": "mf://antlir_snapshots-key@antlir_snapshots/tree/tree/deb/snap-2026-02-27+16:06:14/release.json" }, "checksums": { "mf://antlir_snapshots-key@antlir_snapshots/tree/tree/deb/snap-2026-02-27+16:06:14/archive/dists/trixie/InRelease": { "sha1": "986061afe57bd34842e497fdd7dc654fe3d8941f", "sha256": "94f4e8cbf0bd93ebe686aa8fc95410608613bb832536770fe5ab5c8b45527e49" }, "mf://antlir_snapshots-key@antlir_snapshots/tree/tree/deb/snap-2026-02-27+16:06:14/archive/dists/trixie/contrib/binary-amd64/Packages": { "sha1": "f247cc6a94e2c678b378069aa7df31caad9bb52f", "sha256": "4699ac4502079b1dcbb5933d02c3dc31ae2a67dfbfdde4f4adf69d9382682dc4" }, "mf://antlir_snapshots-key@antlir_snapshots/tree/tree/deb/snap-2026-02-27+16:06:14/archive/dists/trixie/main/binary-amd64/Packages": { "sha1": "5ef3ee32dbc68dce80b6f397b638035c63b871a9", "sha256": "056028ca06f66849ee86f378cb8dc5ad41e6322ea2d2877317921d4d2a50a5ba" }, "mf://antlir_snapshots-key@antlir_snapshots/tree/tree/deb/snap-2026-02-27+16:06:14/components/contrib/packages.json": { "sha1": "59bac48a3ed445a263a07c18087a9d34bc8cb4db", "sha256": "fed74781da90720bd07ff5a355339d63ea817cfca305ff764ccbe0ae0c7e2b03" }, "mf://antlir_snapshots-key@antlir_snapshots/tree/tree/deb/snap-2026-02-27+16:06:14/components/main/packages.json": { "sha1": "f9f98f088fd1427e08c3a45cba209800e1d88e6e", "sha256": "e0845069da0671ba734d1b1bcf8e27fed4bac8095ce005b0aa4b9ca52ae4f615" }, "mf://antlir_snapshots-key@antlir_snapshots/tree/tree/deb/snap-2026-02-27+16:06:14/release.json": { "sha1": "6a80e5e5720e5b1182dcf781d9f74a876405084f", "sha256": "a52749a9b21f1ff161c423e42346f0003ec2aecd19b8c680c5b3799ae8d2fecd" } } } ``` Reviewed By: marcinpe Differential Revision: D93956589 fbshipit-source-id: 16601e1d28b62e1ea9dcbcbba0758c1586c554e7
1 parent dd2c2ab commit df19074

File tree

9 files changed

+311
-13
lines changed

9 files changed

+311
-13
lines changed

antlir/antlir2/package_managers/deb/suite.bzl

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
load("//antlir/antlir2/bzl:platform.bzl", "rule_with_default_target_platform")
77
load("//antlir/antlir2/package_managers/snapshot:download.bzl", "download")
8+
load("//antlir/antlir2/package_managers/snapshot:snapshottable.bzl", "SnapshottableInfo")
89
load(":packages_index.bzl", "download_component_package_indexes")
910

1011
ComponentInfo = provider(fields = {
@@ -37,7 +38,10 @@ def _suite_impl(ctx: AnalysisContext) -> list[Provider]:
3738
allow_nondeterministic_downloads = True,
3839
)
3940

41+
metadata_tree = {}
42+
4043
release_json = ctx.actions.declare_output("release.json", has_content_based_path = True)
44+
metadata_tree["release.json"] = release_json
4145
ctx.actions.run(
4246
cmd_args(
4347
ctx.attrs._snapshot_bin[RunInfo],
@@ -68,6 +72,7 @@ def _suite_impl(ctx: AnalysisContext) -> list[Provider]:
6872
components_subtargets = {}
6973
for cname, c in components_package_indexes.items():
7074
components_subtargets[cname] = [DefaultInfo(sub_targets = {"packages.json": [DefaultInfo(c.json)]})]
75+
metadata_tree["components/" + cname + "/packages.json"] = c.json
7176

7277
# Generate a custom InRelease that only lists the components we care
7378
# about, signed with a dummy key so apt can verify it.
@@ -100,6 +105,8 @@ def _suite_impl(ctx: AnalysisContext) -> list[Provider]:
100105
archive_dir_srcs[dist_prefix + cname + "/binary-" + ctx.attrs._arch + "/Packages"] = c.txt
101106

102107
archive_dir = ctx.actions.symlinked_dir("archive", archive_dir_srcs)
108+
metadata_tree["archive"] = archive_dir
109+
metadata_tree = ctx.actions.symlinked_dir("snapshottable", metadata_tree)
103110

104111
return [
105112
DefaultInfo(sub_targets = {
@@ -117,6 +124,9 @@ def _suite_impl(ctx: AnalysisContext) -> list[Provider]:
117124
components = components,
118125
suite_baseurl = suite_baseurl,
119126
),
127+
SnapshottableInfo(
128+
metadata_tree = metadata_tree,
129+
),
120130
]
121131

122132
_suite = rule(

antlir/antlir2/package_managers/snapshot/BUCK

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,18 @@ oncall("antlir")
55
rust_binary(
66
name = "snapshot",
77
srcs = glob(["src/**/*.rs"]),
8+
crate_root = "src/main.rs",
9+
fb_deps = [
10+
"//manifold/clients/rust:manifold_client",
11+
],
812
test_srcs = glob(["testdata/**"]),
913
deps = [
1014
"anyhow",
15+
"async-trait",
1116
"bon",
1217
"clap",
18+
"fbinit",
19+
"fbinit-tokio",
1320
"flate2",
1421
"hex",
1522
"liblzma",
@@ -20,6 +27,8 @@ rust_binary(
2027
"sha2",
2128
"tracing",
2229
"tracing-subscriber",
30+
"url",
31+
"walkdir",
2332
"//antlir/filesystem/stdio_path:stdio_path",
2433
"//antlir/util/cli/json_arg:json_arg",
2534
],
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
# Copyright (c) Meta Platforms, Inc. and affiliates.
2+
#
3+
# This source code is licensed under the MIT license found in the
4+
# LICENSE file in the root directory of this source tree.
5+
6+
load("@prelude//utils:expect.bzl", "expect")
7+
load("//antlir/antlir2/package_managers/snapshot:snapshottable.bzl", "SnapshottableInfo")
8+
9+
def _main(ctx: bxl.Context):
10+
# Get the snapshot binary as an exec dependency
11+
snapshot_bin = ctx.unconfigured_sub_targets("fbcode//antlir/antlir2/package_managers/snapshot:snapshot")
12+
13+
# Set up BXL actions with the snapshot binary
14+
bxl_actions = ctx.bxl_actions(exec_deps = [snapshot_bin])
15+
actions = bxl_actions.actions
16+
snapshot_run_info = bxl_actions.exec_deps[snapshot_bin][RunInfo]
17+
18+
target_labels = ctx.cli_args.target
19+
expect(len(target_labels) == 1, "Only one target is supported at this time")
20+
target = ctx.configured_targets(target_labels[0], modifiers = ctx.modifiers)
21+
analysis = ctx.analysis(target)
22+
snapshottable_info = analysis.providers()[SnapshottableInfo]
23+
24+
metadata_json = actions.declare_output("metadata.json")
25+
logs = actions.declare_output("logs.txt")
26+
actions.run(
27+
cmd_args(
28+
snapshot_run_info,
29+
cmd_args(logs.as_output(), format = "--log={}"),
30+
"snapshot",
31+
cmd_args(metadata_json.as_output(), format = "--out={}"),
32+
cmd_args(ctx.cli_args.storage, format = "--storage={}"),
33+
"metadata",
34+
cmd_args(snapshottable_info.metadata_tree, format = "--tree={}"),
35+
),
36+
category = "snapshot",
37+
identifier = "metadata",
38+
local_only = True, # needs network access
39+
allow_cache_upload = True, # the result is deterministic
40+
)
41+
ctx.output.stream("Metadata snapshot:", ctx.output.ensure(metadata_json), wait_on = [ctx.output.ensure(metadata_json)])
42+
43+
main = bxl_main(
44+
impl = _main,
45+
cli_args = {
46+
"storage": cli_args.string(),
47+
"target": cli_args.target_expr(),
48+
},
49+
)
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
# Copyright (c) Meta Platforms, Inc. and affiliates.
2+
#
3+
# This source code is licensed under the MIT license found in the
4+
# LICENSE file in the root directory of this source tree.
5+
6+
SnapshottableInfo = provider(fields = {
7+
# A directory artifact that should contain all the metadata artifacts that
8+
# should be snapshotted into persistent storage. This is considered cheap to
9+
# produce and store, as opposed to package blobs (which are belligerent and
10+
# numerous https://www.youtube.com/watch?v=HvVJQMnB-N8), so the target that
11+
# contains this provider should just produce the entire tree of metadata
12+
# artifacts every time.
13+
"metadata_tree": Artifact,
14+
})

antlir/antlir2/package_managers/snapshot/src/checksums.rs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,3 +69,23 @@ impl Checksums {
6969
}
7070
}
7171
}
72+
73+
#[cfg(test)]
74+
mod tests {
75+
use super::*;
76+
77+
#[test]
78+
fn test_from_reader() {
79+
let input = "Hello world\n".as_bytes();
80+
let checksums = Checksums::from_reader(input).expect("failed to compute checksums");
81+
assert_eq!(
82+
checksums,
83+
Checksums {
84+
sha1: Some("33ab5639bfd8e7b95eb1d8d0b87781d4ffea4d5d".to_string()),
85+
sha256: Some(
86+
"1894a19c85ba153acbf743ac4e43fc004c891604b26f8c69e1e83ea2afc7c48f".to_string()
87+
),
88+
}
89+
);
90+
}
91+
}

antlir/antlir2/package_managers/snapshot/src/main.rs

Lines changed: 33 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,10 @@
55
* LICENSE file in the root directory of this source tree.
66
*/
77

8+
use std::fs::File;
9+
use std::path::PathBuf;
810
use std::process::ExitCode;
11+
use std::sync::Mutex;
912

1013
use anyhow::Result;
1114
use clap::Parser;
@@ -16,48 +19,65 @@ mod checksums;
1619
mod decompress;
1720
mod generate;
1821
mod parse;
22+
mod snapshot;
1923

2024
#[derive(Debug, Parser)]
2125
struct Args {
2226
#[clap(short, long, default_value_t=1, action = clap::ArgAction::Count)]
2327
verbose: u8,
2428
#[clap(subcommand)]
2529
sub: Sub,
30+
#[clap(long)]
31+
log: Option<PathBuf>,
2632
}
2733

2834
#[derive(Debug, Parser)]
2935
enum Sub {
3036
Decompress(decompress::Decompress),
3137
Generate(generate::Generate),
3238
Parse(parse::Parse),
39+
Snapshot(snapshot::Snapshot),
3340
}
3441

35-
fn main() -> ExitCode {
42+
#[fbinit::main]
43+
async fn main(fb: fbinit::FacebookInit) -> ExitCode {
3644
// Wrap the real main function with this so that we can print out the full
3745
// error
38-
if let Err(e) = do_main() {
46+
if let Err(e) = do_main(fb).await {
3947
error!("{e:#?}");
4048
return ExitCode::FAILURE;
4149
}
4250
ExitCode::SUCCESS
4351
}
4452

45-
fn do_main() -> Result<()> {
53+
async fn do_main(fb: fbinit::FacebookInit) -> Result<()> {
4654
let args = Args::parse();
47-
tracing_subscriber::FmtSubscriber::builder()
48-
.with_max_level(match args.verbose {
49-
0 => tracing::Level::ERROR,
50-
1 => tracing::Level::WARN,
51-
2 => tracing::Level::INFO,
52-
3 => tracing::Level::DEBUG,
53-
_ => tracing::Level::TRACE,
54-
})
55-
.finish()
55+
let stderr_level = match args.verbose {
56+
0 => tracing_subscriber::filter::LevelFilter::ERROR,
57+
1 => tracing_subscriber::filter::LevelFilter::WARN,
58+
2 => tracing_subscriber::filter::LevelFilter::INFO,
59+
3 => tracing_subscriber::filter::LevelFilter::DEBUG,
60+
_ => tracing_subscriber::filter::LevelFilter::TRACE,
61+
};
62+
let stderr_layer = tracing_subscriber::fmt::layer().with_filter(stderr_level);
63+
let file_layer = if let Some(log) = &args.log {
64+
let file = File::create(log)?;
65+
Some(
66+
tracing_subscriber::fmt::layer()
67+
.with_writer(Mutex::new(file))
68+
.with_filter(tracing_subscriber::filter::LevelFilter::DEBUG),
69+
)
70+
} else {
71+
None
72+
};
73+
tracing_subscriber::registry()
74+
.with(stderr_layer)
75+
.with(file_layer)
5676
.init();
57-
5877
match args.sub {
5978
Sub::Decompress(sub) => sub.run(),
6079
Sub::Generate(sub) => sub.run(),
6180
Sub::Parse(sub) => sub.run(),
81+
Sub::Snapshot(sub) => sub.run(fb).await,
6282
}
6383
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
/*
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
use std::collections::BTreeMap;
9+
use std::io::BufWriter;
10+
use std::path::PathBuf;
11+
12+
use anyhow::Result;
13+
use clap::Parser;
14+
use clap::Subcommand;
15+
use json_arg::Json;
16+
use serde::Serialize;
17+
18+
use crate::checksums::Checksums;
19+
20+
mod metadata;
21+
mod storage;
22+
use storage::StorageConfig;
23+
24+
#[derive(Debug, Parser)]
25+
pub(crate) struct Snapshot {
26+
#[clap(subcommand)]
27+
sub: Sub,
28+
#[clap(long)]
29+
out: PathBuf,
30+
#[clap(long)]
31+
storage: Json<StorageConfig>,
32+
}
33+
34+
#[derive(Debug, Serialize)]
35+
pub(super) struct Out {
36+
pub(super) files: BTreeMap<String, String>,
37+
pub(super) checksums: BTreeMap<String, Checksums>,
38+
}
39+
40+
#[derive(Debug, Subcommand)]
41+
enum Sub {
42+
/// Snapshot metadata tree
43+
Metadata(metadata::Metadata),
44+
}
45+
46+
impl Snapshot {
47+
pub(crate) async fn run(self, fb: fbinit::FacebookInit) -> Result<()> {
48+
let storage = self.storage.into_inner().build(fb)?;
49+
let out = match &self.sub {
50+
Sub::Metadata(metadata) => metadata.run(storage).await,
51+
}?;
52+
let outfile = BufWriter::new(stdio_path::create(&self.out)?);
53+
serde_json::to_writer(outfile, &out)?;
54+
Ok(())
55+
}
56+
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
/*
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
use std::collections::BTreeMap;
9+
use std::path::PathBuf;
10+
11+
use anyhow::Context;
12+
use anyhow::Result;
13+
use clap::Parser;
14+
use tracing::debug;
15+
use walkdir::WalkDir;
16+
17+
use super::Out;
18+
use super::storage::Storage;
19+
20+
#[derive(Parser, Debug)]
21+
pub(crate) struct Metadata {
22+
#[clap(long)]
23+
tree: PathBuf,
24+
}
25+
26+
impl Metadata {
27+
#[tracing::instrument(skip(self, storage), ret, err)]
28+
pub(crate) async fn run(&self, storage: Box<dyn Storage>) -> Result<Out> {
29+
let mut files = BTreeMap::new();
30+
let mut checksums = BTreeMap::new();
31+
debug!("walking {self:?}");
32+
33+
for entry in WalkDir::new(&self.tree).follow_links(true) {
34+
let entry = entry.context("failed to walk metadata tree")?;
35+
debug!("processing {entry:?}");
36+
if !entry.file_type().is_file() {
37+
debug!("skipping because file type is {:?}", entry.file_type());
38+
continue;
39+
}
40+
let relative = entry
41+
.path()
42+
.strip_prefix(&self.tree)
43+
.context("failed to compute relative path")?;
44+
let key = relative.to_str().context("non-utf8 path")?;
45+
let result = storage.store(entry.path(), key).await?;
46+
let url = result.url.to_string();
47+
checksums.insert(url.clone(), result.checksums);
48+
files.insert(key.to_owned(), url);
49+
}
50+
51+
Ok(Out { files, checksums })
52+
}
53+
}

0 commit comments

Comments
 (0)