Skip to content

Commit 5254dbf

Browse files
authored
Merge pull request #105 from rust-lang/dynamic-compression
Allow specifying the wanted compression formats at runtime
2 parents 27fed98 + 512629a commit 5254dbf

File tree

9 files changed

+405
-75
lines changed

9 files changed

+405
-75
lines changed

src/combiner.rs

+25-13
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
use super::Scripter;
22
use super::Tarballer;
3-
use crate::util::*;
3+
use crate::{
4+
compression::{CompressionFormat, CompressionFormats},
5+
util::*,
6+
};
47
use anyhow::{bail, Context, Result};
5-
use flate2::read::GzDecoder;
68
use std::io::{Read, Write};
79
use std::path::Path;
810
use tar::Archive;
@@ -36,6 +38,9 @@ actor! {
3638

3739
/// The location to put the final image and tarball.
3840
output_dir: String = "./dist",
41+
42+
/// The formats used to compress the tarball
43+
compression_formats: CompressionFormats = CompressionFormats::default(),
3944
}
4045
}
4146

@@ -59,15 +64,21 @@ impl Combiner {
5964
.filter(|s| !s.is_empty())
6065
{
6166
// Extract the input tarballs
62-
let tar = GzDecoder::new(open_file(&input_tarball)?);
63-
Archive::new(tar).unpack(&self.work_dir).with_context(|| {
64-
format!(
65-
"unable to extract '{}' into '{}'",
66-
&input_tarball, self.work_dir
67-
)
68-
})?;
69-
70-
let pkg_name = input_tarball.trim_end_matches(".tar.gz");
67+
let compression =
68+
CompressionFormat::detect_from_path(input_tarball).ok_or_else(|| {
69+
anyhow::anyhow!("couldn't figure out the format of {}", input_tarball)
70+
})?;
71+
Archive::new(compression.decode(input_tarball)?)
72+
.unpack(&self.work_dir)
73+
.with_context(|| {
74+
format!(
75+
"unable to extract '{}' into '{}'",
76+
&input_tarball, self.work_dir
77+
)
78+
})?;
79+
80+
let pkg_name =
81+
input_tarball.trim_end_matches(&format!(".tar.{}", compression.extension()));
7182
let pkg_name = Path::new(pkg_name).file_name().unwrap();
7283
let pkg_dir = Path::new(&self.work_dir).join(&pkg_name);
7384

@@ -121,7 +132,7 @@ impl Combiner {
121132
.rel_manifest_dir(self.rel_manifest_dir)
122133
.success_message(self.success_message)
123134
.legacy_manifest_dirs(self.legacy_manifest_dirs)
124-
.output_script(path_to_str(&output_script)?);
135+
.output_script(path_to_str(&output_script)?.into());
125136
scripter.run()?;
126137

127138
// Make the tarballs.
@@ -131,7 +142,8 @@ impl Combiner {
131142
tarballer
132143
.work_dir(self.work_dir)
133144
.input(self.package_name)
134-
.output(path_to_str(&output)?);
145+
.output(path_to_str(&output)?.into())
146+
.compression_formats(self.compression_formats.clone());
135147
tarballer.run()?;
136148

137149
Ok(())

src/compression.rs

+154
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
use anyhow::{Context, Error};
2+
use flate2::{read::GzDecoder, write::GzEncoder};
3+
use rayon::prelude::*;
4+
use std::{convert::TryFrom, io::Read, io::Write, path::Path};
5+
use xz2::{read::XzDecoder, write::XzEncoder};
6+
7+
#[derive(Debug, Copy, Clone)]
8+
pub enum CompressionFormat {
9+
Gz,
10+
Xz,
11+
}
12+
13+
impl CompressionFormat {
14+
pub(crate) fn detect_from_path(path: impl AsRef<Path>) -> Option<Self> {
15+
match path.as_ref().extension().and_then(|e| e.to_str()) {
16+
Some("gz") => Some(CompressionFormat::Gz),
17+
Some("xz") => Some(CompressionFormat::Xz),
18+
_ => None,
19+
}
20+
}
21+
22+
pub(crate) fn extension(&self) -> &'static str {
23+
match self {
24+
CompressionFormat::Gz => "gz",
25+
CompressionFormat::Xz => "xz",
26+
}
27+
}
28+
29+
pub(crate) fn encode(&self, path: impl AsRef<Path>) -> Result<Box<dyn Encoder>, Error> {
30+
let mut os = path.as_ref().as_os_str().to_os_string();
31+
os.push(format!(".{}", self.extension()));
32+
let path = Path::new(&os);
33+
34+
if path.exists() {
35+
crate::util::remove_file(path)?;
36+
}
37+
let file = crate::util::create_new_file(path)?;
38+
39+
Ok(match self {
40+
CompressionFormat::Gz => Box::new(GzEncoder::new(file, flate2::Compression::best())),
41+
CompressionFormat::Xz => {
42+
// Note that preset 6 takes about 173MB of memory per thread, so we limit the number of
43+
// threads to not blow out 32-bit hosts. (We could be more precise with
44+
// `MtStreamBuilder::memusage()` if desired.)
45+
let stream = xz2::stream::MtStreamBuilder::new()
46+
.threads(Ord::min(num_cpus::get(), 8) as u32)
47+
.preset(6)
48+
.encoder()?;
49+
Box::new(XzEncoder::new_stream(file, stream))
50+
}
51+
})
52+
}
53+
54+
pub(crate) fn decode(&self, path: impl AsRef<Path>) -> Result<Box<dyn Read>, Error> {
55+
let file = crate::util::open_file(path.as_ref())?;
56+
Ok(match self {
57+
CompressionFormat::Gz => Box::new(GzDecoder::new(file)),
58+
CompressionFormat::Xz => Box::new(XzDecoder::new(file)),
59+
})
60+
}
61+
}
62+
63+
/// This struct wraps Vec<CompressionFormat> in order to parse the value from the command line.
64+
#[derive(Debug, Clone)]
65+
pub struct CompressionFormats(Vec<CompressionFormat>);
66+
67+
impl TryFrom<&'_ str> for CompressionFormats {
68+
type Error = Error;
69+
70+
fn try_from(value: &str) -> Result<Self, Self::Error> {
71+
let mut parsed = Vec::new();
72+
for format in value.split(',') {
73+
match format.trim() {
74+
"gz" => parsed.push(CompressionFormat::Gz),
75+
"xz" => parsed.push(CompressionFormat::Xz),
76+
other => anyhow::bail!("unknown compression format: {}", other),
77+
}
78+
}
79+
Ok(CompressionFormats(parsed))
80+
}
81+
}
82+
83+
impl Default for CompressionFormats {
84+
fn default() -> Self {
85+
Self(vec![CompressionFormat::Gz, CompressionFormat::Xz])
86+
}
87+
}
88+
89+
impl CompressionFormats {
90+
pub(crate) fn iter(&self) -> impl Iterator<Item = CompressionFormat> + '_ {
91+
self.0.iter().map(|i| *i)
92+
}
93+
}
94+
95+
pub(crate) trait Encoder: Send + Write {
96+
fn finish(self: Box<Self>) -> Result<(), Error>;
97+
}
98+
99+
impl<W: Send + Write> Encoder for GzEncoder<W> {
100+
fn finish(self: Box<Self>) -> Result<(), Error> {
101+
GzEncoder::finish(*self).context("failed to finish .gz file")?;
102+
Ok(())
103+
}
104+
}
105+
106+
impl<W: Send + Write> Encoder for XzEncoder<W> {
107+
fn finish(self: Box<Self>) -> Result<(), Error> {
108+
XzEncoder::finish(*self).context("failed to finish .xz file")?;
109+
Ok(())
110+
}
111+
}
112+
113+
pub(crate) struct CombinedEncoder {
114+
encoders: Vec<Box<dyn Encoder>>,
115+
}
116+
117+
impl CombinedEncoder {
118+
pub(crate) fn new(encoders: Vec<Box<dyn Encoder>>) -> Box<dyn Encoder> {
119+
Box::new(Self { encoders })
120+
}
121+
}
122+
123+
impl Write for CombinedEncoder {
124+
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
125+
self.write_all(buf)?;
126+
Ok(buf.len())
127+
}
128+
129+
fn write_all(&mut self, buf: &[u8]) -> std::io::Result<()> {
130+
self.encoders
131+
.par_iter_mut()
132+
.map(|w| w.write_all(buf))
133+
.collect::<std::io::Result<Vec<()>>>()?;
134+
Ok(())
135+
}
136+
137+
fn flush(&mut self) -> std::io::Result<()> {
138+
self.encoders
139+
.par_iter_mut()
140+
.map(|w| w.flush())
141+
.collect::<std::io::Result<Vec<()>>>()?;
142+
Ok(())
143+
}
144+
}
145+
146+
impl Encoder for CombinedEncoder {
147+
fn finish(self: Box<Self>) -> Result<(), Error> {
148+
self.encoders
149+
.into_par_iter()
150+
.map(|e| e.finish())
151+
.collect::<Result<Vec<()>, Error>>()?;
152+
Ok(())
153+
}
154+
}

src/generator.rs

+7-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
use super::Scripter;
22
use super::Tarballer;
3+
use crate::compression::CompressionFormats;
34
use crate::util::*;
45
use anyhow::{bail, format_err, Context, Result};
56
use std::io::Write;
@@ -40,6 +41,9 @@ actor! {
4041

4142
/// The location to put the final image and tarball
4243
output_dir: String = "./dist",
44+
45+
/// The formats used to compress the tarball
46+
compression_formats: CompressionFormats = CompressionFormats::default(),
4347
}
4448
}
4549

@@ -85,7 +89,7 @@ impl Generator {
8589
.rel_manifest_dir(self.rel_manifest_dir)
8690
.success_message(self.success_message)
8791
.legacy_manifest_dirs(self.legacy_manifest_dirs)
88-
.output_script(path_to_str(&output_script)?);
92+
.output_script(path_to_str(&output_script)?.into());
8993
scripter.run()?;
9094

9195
// Make the tarballs
@@ -95,7 +99,8 @@ impl Generator {
9599
tarballer
96100
.work_dir(self.work_dir)
97101
.input(self.package_name)
98-
.output(path_to_str(&output)?);
102+
.output(path_to_str(&output)?.into())
103+
.compression_formats(self.compression_formats.clone());
99104
tarballer.run()?;
100105

101106
Ok(())

src/lib.rs

+1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
mod util;
33

44
mod combiner;
5+
mod compression;
56
mod generator;
67
mod scripter;
78
mod tarballer;

src/main.rs

+9-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
use anyhow::{Context, Result};
22
use clap::{App, ArgMatches};
3+
use std::convert::TryInto;
34

45
fn main() -> Result<()> {
56
let yaml = clap::load_yaml!("main.yml");
@@ -19,7 +20,11 @@ macro_rules! parse(
1920
($matches:expr => $type:ty { $( $option:tt => $setter:ident, )* }) => {
2021
{
2122
let mut command: $type = Default::default();
22-
$( $matches.value_of($option).map(|s| command.$setter(s)); )*
23+
$(
24+
if let Some(val) = $matches.value_of($option) {
25+
command.$setter(val.try_into()?);
26+
}
27+
)*
2328
command
2429
}
2530
}
@@ -36,6 +41,7 @@ fn combine(matches: &ArgMatches<'_>) -> Result<()> {
3641
"non-installed-overlay" => non_installed_overlay,
3742
"work-dir" => work_dir,
3843
"output-dir" => output_dir,
44+
"compression-formats" => compression_formats,
3945
});
4046

4147
combiner.run().context("failed to combine installers")?;
@@ -55,6 +61,7 @@ fn generate(matches: &ArgMatches<'_>) -> Result<()> {
5561
"image-dir" => image_dir,
5662
"work-dir" => work_dir,
5763
"output-dir" => output_dir,
64+
"compression-formats" => compression_formats,
5865
});
5966

6067
generator.run().context("failed to generate installer")?;
@@ -81,6 +88,7 @@ fn tarball(matches: &ArgMatches<'_>) -> Result<()> {
8188
"input" => input,
8289
"output" => output,
8390
"work-dir" => work_dir,
91+
"compression-formats" => compression_formats,
8492
});
8593

8694
tarballer.run().context("failed to generate tarballs")?;

src/main.yml

+15
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,11 @@ subcommands:
6060
long: output-dir
6161
takes_value: true
6262
value_name: DIR
63+
- compression-formats:
64+
help: Comma-separated list of compression formats to use
65+
long: compression-formats
66+
takes_value: true
67+
value_name: FORMAT
6368
- combine:
6469
about: Combine installer tarballs
6570
args:
@@ -108,6 +113,11 @@ subcommands:
108113
long: output-dir
109114
takes_value: true
110115
value_name: DIR
116+
- compression-formats:
117+
help: Comma-separated list of compression formats to use
118+
long: compression-formats
119+
takes_value: true
120+
value_name: FORMAT
111121
- script:
112122
about: Generate an installation script
113123
args:
@@ -154,4 +164,9 @@ subcommands:
154164
long: work-dir
155165
takes_value: true
156166
value_name: DIR
167+
- compression-formats:
168+
help: Comma-separated list of compression formats to use
169+
long: compression-formats
170+
takes_value: true
171+
value_name: FORMAT
157172

0 commit comments

Comments
 (0)