@@ -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
2022fn 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