Skip to content

Commit 65d76fa

Browse files
committed
contrib/packaging: New spec file
This mirrors exactly what we're doing in bootc, motivated by prep for shipping this in Fedora (including EPEL). Signed-off-by: Colin Walters <walters@verbum.org>
1 parent aa47895 commit 65d76fa

2 files changed

Lines changed: 299 additions & 42 deletions

File tree

contrib/packaging/bcvk.spec

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
%bcond_without check
2+
3+
Name: bcvk
4+
Version: 0.5.3
5+
Release: 1%{?dist}
6+
Summary: Bootable container VM toolkit
7+
8+
# Apache-2.0 OR MIT
9+
License: Apache-2.0 OR MIT
10+
URL: https://github.com/bootc-dev/bcvk
11+
Source0: %{url}/releases/download/v%{version}/bcvk-%{version}.tar.zstd
12+
Source1: %{url}/releases/download/v%{version}/bcvk-%{version}-vendor.tar.zstd
13+
14+
# https://fedoraproject.org/wiki/Changes/EncourageI686LeafRemoval
15+
ExcludeArch: %{ix86}
16+
17+
BuildRequires: make
18+
BuildRequires: openssl-devel
19+
BuildRequires: go-md2man
20+
%if 0%{?rhel}
21+
BuildRequires: rust-toolset
22+
%else
23+
BuildRequires: cargo-rpm-macros >= 25
24+
%endif
25+
26+
%description
27+
%{summary}
28+
29+
%prep
30+
%autosetup -p1 -a1
31+
# Default -v vendor config doesn't support non-crates.io deps (i.e. git)
32+
cp .cargo/vendor-config.toml .
33+
%cargo_prep -N
34+
cat vendor-config.toml >> .cargo/config.toml
35+
rm vendor-config.toml
36+
37+
%build
38+
%cargo_build
39+
40+
make manpages
41+
42+
%cargo_vendor_manifest
43+
# https://pagure.io/fedora-rust/rust-packaging/issue/33
44+
sed -i -e '/https:\/\//d' cargo-vendor.txt
45+
%cargo_license_summary
46+
%{cargo_license} > LICENSE.dependencies
47+
48+
%install
49+
%make_install INSTALL="install -p -c"
50+
51+
%if %{with check}
52+
%check
53+
%cargo_test
54+
%endif
55+
56+
%files
57+
%license LICENSE-MIT
58+
%license LICENSE-APACHE
59+
%license LICENSE.dependencies
60+
%license cargo-vendor.txt
61+
%doc README.md
62+
%{_bindir}/bcvk
63+
%{_mandir}/man*/*bcvk*
64+
65+
%changelog
66+
%autochangelog

crates/xtask/src/xtask.rs

Lines changed: 233 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ const TASKS: &[(&str, fn(&Shell) -> Result<()>)] = &[
1515
("update-manpages", update_manpages),
1616
("sync-manpages", sync_manpages),
1717
("package", package),
18+
("package-srpm", package_srpm),
19+
("spec", spec),
1820
];
1921

2022
fn install_tracing() {
@@ -86,65 +88,254 @@ fn sync_manpages(sh: &Shell) -> Result<()> {
8688
man::sync_all_man_pages(sh)
8789
}
8890

89-
fn package(sh: &Shell) -> Result<()> {
90-
use std::env;
91+
const NAME: &str = "bcvk";
92+
const TAR_REPRODUCIBLE_OPTS: &[&str] = &[
93+
"--sort=name",
94+
"--owner=0",
95+
"--group=0",
96+
"--numeric-owner",
97+
"--pax-option=exthdr.name=%d/PaxHeaders/%f,delete=atime,delete=ctime",
98+
];
99+
100+
fn gitrev_to_version(v: &str) -> String {
101+
let v = v.trim().trim_start_matches('v');
102+
v.replace('-', ".")
103+
}
104+
105+
fn gitrev(sh: &Shell) -> Result<String> {
91106
use xshell::cmd;
107+
if let Ok(rev) = cmd!(sh, "git describe --tags --exact-match")
108+
.ignore_stderr()
109+
.read()
110+
{
111+
Ok(gitrev_to_version(&rev))
112+
} else {
113+
// Just use the version from Cargo.toml
114+
man::get_raw_package_version()
115+
}
116+
}
92117

93-
// Get version from Cargo.toml
94-
let version = man::get_raw_package_version()?;
118+
/// Return the timestamp of the latest git commit in seconds since the Unix epoch.
119+
fn git_source_date_epoch(dir: &str) -> Result<u64> {
120+
let o = Command::new("git")
121+
.args(["log", "-1", "--pretty=%ct"])
122+
.current_dir(dir)
123+
.output()?;
124+
if !o.status.success() {
125+
return Err(eyre!("git exited with an error: {:?}", o));
126+
}
127+
let buf = String::from_utf8(o.stdout).context("Failed to parse git log output")?;
128+
let r = buf.trim().parse()?;
129+
Ok(r)
130+
}
95131

96-
println!("Creating release archives for version {}", version);
132+
/// When using cargo-vendor-filterer --format=tar, the config generated has a bogus source
133+
/// directory. This edits it to refer to vendor/ as a stable relative reference.
134+
fn edit_vendor_config(config: &str) -> Result<String> {
135+
let mut config: toml::Value = toml::from_str(config)?;
136+
let config = config
137+
.as_table_mut()
138+
.ok_or_else(|| eyre!("Vendor config is not a table"))?;
139+
let source_table = config
140+
.get_mut("source")
141+
.ok_or_else(|| eyre!("Missing 'source' key in vendor config"))?
142+
.as_table_mut()
143+
.ok_or_else(|| eyre!("'source' is not a table"))?;
144+
let vendored_sources = source_table
145+
.get_mut("vendored-sources")
146+
.ok_or_else(|| eyre!("Missing 'vendored-sources' key"))?
147+
.as_table_mut()
148+
.ok_or_else(|| eyre!("'vendored-sources' is not a table"))?;
149+
let previous =
150+
vendored_sources.insert("directory".into(), toml::Value::String("vendor".into()));
151+
if previous.is_none() {
152+
return Err(eyre!("Expected 'directory' key in vendored-sources"));
153+
}
97154

98-
// Get the git commit timestamp for reproducible builds
99-
let source_date_epoch = cmd!(sh, "git log -1 --format=%ct").read()?;
100-
env::set_var("SOURCE_DATE_EPOCH", source_date_epoch.trim());
155+
Ok(config.to_string())
156+
}
101157

102-
// Create target directory if it doesn't exist
103-
sh.create_dir("target")?;
158+
struct Package {
159+
version: String,
160+
srcpath: camino::Utf8PathBuf,
161+
vendorpath: camino::Utf8PathBuf,
162+
}
104163

105-
// Create temporary directory for intermediate files
106-
let tempdir = tempfile::tempdir()?;
107-
let temp_tar = tempdir.path().join(format!("bcvk-{}.tar", version));
164+
fn impl_package(sh: &Shell) -> Result<Package> {
165+
use camino::Utf8Path;
166+
use xshell::cmd;
108167

109-
// Create source archive using git archive (uncompressed initially)
110-
let source_archive = format!("target/bcvk-{}.tar.zstd", version);
111-
cmd!(
112-
sh,
113-
"git archive --format=tar --prefix=bcvk-{version}/ HEAD -o {temp_tar}"
114-
)
115-
.run()?;
168+
let source_date_epoch = git_source_date_epoch(".")?;
169+
let v = gitrev(sh)?;
116170

117-
// Create vendor archive
118-
let vendor_archive = format!("target/bcvk-{}-vendor.tar.zstd", version);
119-
cmd!(
171+
let namev = format!("{NAME}-{v}");
172+
let p = Utf8Path::new("target").join(format!("{namev}.tar"));
173+
let prefix = format!("{namev}/");
174+
cmd!(sh, "git archive --format=tar --prefix={prefix} -o {p} HEAD").run()?;
175+
// Generate the vendor directory now, as we want to embed the generated config to use
176+
// it in our source.
177+
let vendorpath = Utf8Path::new("target").join(format!("{namev}-vendor.tar.zstd"));
178+
let vendor_config = cmd!(
120179
sh,
121-
"cargo vendor-filterer --format=tar.zstd {vendor_archive}"
180+
"cargo vendor-filterer --prefix=vendor --format=tar.zstd {vendorpath}"
122181
)
123-
.run()?;
182+
.read()?;
183+
let vendor_config = edit_vendor_config(&vendor_config)?;
184+
// Append .cargo/vendor-config.toml (a made up filename) into the tar archive.
185+
{
186+
let tmpdir = tempfile::tempdir_in("target")?;
187+
let tmpdir_path = tmpdir.path();
188+
let path = tmpdir_path.join("vendor-config.toml");
189+
std::fs::write(&path, vendor_config)?;
190+
let source_date_epoch = format!("{source_date_epoch}");
191+
cmd!(
192+
sh,
193+
"tar -r -C {tmpdir_path} {TAR_REPRODUCIBLE_OPTS...} --mtime=@{source_date_epoch} --transform=s,^,{prefix}.cargo/, -f {p} vendor-config.toml"
194+
)
195+
.run()?;
196+
}
197+
// Compress with zstd
198+
let srcpath: camino::Utf8PathBuf = format!("{p}.zstd").into();
199+
cmd!(sh, "zstd --rm -f {p} -o {srcpath}").run()?;
124200

125-
println!("Created vendor archive: {}", vendor_archive);
201+
Ok(Package {
202+
version: v,
203+
srcpath,
204+
vendorpath,
205+
})
206+
}
126207

127-
// Create vendor config for the source archive
128-
let vendor_config_content = r#"[source.crates-io]
129-
replace-with = "vendored-sources"
208+
fn package(sh: &Shell) -> Result<()> {
209+
let p = impl_package(sh)?.srcpath;
210+
println!("Generated: {p}");
211+
Ok(())
212+
}
130213

131-
[source.vendored-sources]
132-
directory = "vendor"
133-
"#;
134-
let vendor_config_path = tempdir.path().join(".cargo-vendor-config.toml");
135-
std::fs::write(&vendor_config_path, vendor_config_content)?;
214+
fn update_spec(sh: &Shell) -> Result<camino::Utf8PathBuf> {
215+
use camino::Utf8Path;
216+
use std::fs::File;
217+
use std::io::{BufRead, BufReader, BufWriter, Write};
136218

137-
// Add vendor config to source archive
138-
cmd!(sh, "tar --owner=0 --group=0 --numeric-owner --sort=name --mtime=@{source_date_epoch} -rf {temp_tar} --transform='s|.*/.cargo-vendor-config.toml|bcvk-{version}/.cargo/vendor-config.toml|' {vendor_config_path}").run()?;
219+
let p = Utf8Path::new("target");
220+
let pkg = impl_package(sh)?;
221+
let srcpath = pkg.srcpath.file_name().unwrap();
222+
let v = pkg.version;
223+
let src_vendorpath = pkg.vendorpath.file_name().unwrap();
224+
{
225+
let specin = File::open(format!("contrib/packaging/{NAME}.spec"))
226+
.map(BufReader::new)
227+
.context("Opening spec")?;
228+
let mut o = File::create(p.join(format!("{NAME}.spec"))).map(BufWriter::new)?;
229+
for line in specin.lines() {
230+
let line = line?;
231+
if line.starts_with("Version:") {
232+
writeln!(o, "# Replaced by cargo xtask spec")?;
233+
writeln!(o, "Version: {v}")?;
234+
} else if line.starts_with("Source0") {
235+
writeln!(o, "Source0: {srcpath}")?;
236+
} else if line.starts_with("Source1") {
237+
writeln!(o, "Source1: {src_vendorpath}")?;
238+
} else {
239+
writeln!(o, "{line}")?;
240+
}
241+
}
242+
}
243+
let spec_path = p.join(format!("{NAME}.spec"));
244+
Ok(spec_path)
245+
}
139246

140-
// Compress the final source archive
141-
cmd!(sh, "zstd {temp_tar} -f -o {source_archive}").run()?;
247+
fn spec(sh: &Shell) -> Result<()> {
248+
let s = update_spec(sh)?;
249+
println!("Generated: {s}");
250+
Ok(())
251+
}
142252

143-
println!("Created source archive: {}", source_archive);
253+
fn impl_srpm(sh: &Shell) -> Result<camino::Utf8PathBuf> {
254+
use camino::Utf8Path;
255+
use std::fs::File;
256+
use std::io::{BufRead, BufReader, BufWriter, Write};
257+
use xshell::cmd;
144258

145-
println!("Release archives created successfully:");
146-
println!(" Source: {}", source_archive);
147-
println!(" Vendor: {}", vendor_archive);
259+
{
260+
let _g = sh.push_dir("target");
261+
for name in sh.read_dir(".")? {
262+
if let Some(name) = name.to_str() {
263+
if name.ends_with(".src.rpm") {
264+
sh.remove_path(name)?;
265+
}
266+
}
267+
}
268+
}
269+
let pkg = impl_package(sh)?;
270+
let td = tempfile::tempdir_in("target").context("Allocating tmpdir")?;
271+
let td = td.keep();
272+
let td: &Utf8Path = td
273+
.as_path()
274+
.try_into()
275+
.context("Converting tmpdir path to UTF-8")?;
276+
let srcpath = &pkg.srcpath;
277+
cmd!(sh, "mv {srcpath} {td}").run()?;
278+
let v = pkg.version;
279+
let src_vendorpath = &pkg.vendorpath;
280+
cmd!(sh, "mv {src_vendorpath} {td}").run()?;
281+
let srcpath_filename = pkg.srcpath.file_name().unwrap();
282+
let vendorpath_filename = pkg.vendorpath.file_name().unwrap();
283+
{
284+
let specin = File::open(format!("contrib/packaging/{NAME}.spec"))
285+
.map(BufReader::new)
286+
.context("Opening spec")?;
287+
let mut o = File::create(td.join(format!("{NAME}.spec"))).map(BufWriter::new)?;
288+
for line in specin.lines() {
289+
let line = line?;
290+
if line.starts_with("Version:") {
291+
writeln!(o, "# Replaced by cargo xtask package-srpm")?;
292+
writeln!(o, "Version: {v}")?;
293+
} else if line.starts_with("Source0") {
294+
writeln!(o, "Source0: {srcpath_filename}")?;
295+
} else if line.starts_with("Source1") {
296+
writeln!(o, "Source1: {vendorpath_filename}")?;
297+
} else {
298+
writeln!(o, "{line}")?;
299+
}
300+
}
301+
}
302+
let d = sh.push_dir(td);
303+
let mut cmd = cmd!(sh, "rpmbuild");
304+
for k in [
305+
"_sourcedir",
306+
"_specdir",
307+
"_builddir",
308+
"_srcrpmdir",
309+
"_rpmdir",
310+
] {
311+
cmd = cmd.arg("--define");
312+
cmd = cmd.arg(format!("{k} {td}"));
313+
}
314+
cmd.arg("--define")
315+
.arg(format!("_buildrootdir {td}/.build"))
316+
.args(["-bs", &format!("{NAME}.spec")])
317+
.run()?;
318+
drop(d);
319+
let mut srpm = None;
320+
for e in std::fs::read_dir(td)? {
321+
let e = e?;
322+
let n = e.file_name();
323+
let Some(n) = n.to_str() else {
324+
continue;
325+
};
326+
if n.ends_with(".src.rpm") {
327+
srpm = Some(Utf8Path::new(td).join(n));
328+
break;
329+
}
330+
}
331+
let srpm = srpm.ok_or_else(|| eyre!("Failed to find generated .src.rpm"))?;
332+
let dest = Utf8Path::new("target").join(srpm.file_name().unwrap());
333+
std::fs::rename(&srpm, &dest)?;
334+
Ok(dest)
335+
}
148336

337+
fn package_srpm(sh: &Shell) -> Result<()> {
338+
let srpm = impl_srpm(sh)?;
339+
println!("Generated: {srpm}");
149340
Ok(())
150341
}

0 commit comments

Comments
 (0)