1mod imports;
10
11pub mod immutable;
12pub mod utils;
13
14mod bundle;
15mod html;
16
17pub use crate::bundle::*;
18
19use html::*;
20pub use html::{Html,HtmlRef};
21
22use crate::immutable::Immutable;
23use crate::imports::*;
24use crate::utils::*;
25
26#[derive(Error,Debug)]
27pub enum Error {
28 #[error("output leafname {0:?} has bad syntax")]
29 BadLeafBasename(String),
30
31 #[error("output leafname {leafname:?} reused ({first} vs {second})")]
32 LeafReused { leafname : String, first : String, second : String },
33
34 #[error("full leafname {leafname:?} generated twice by same bundle from different basenames, now due to bundle {bundle_id} - bundle id is not unique or seems to generate inconsistent suffix, or something?")]
35 ComponentLeafGeneratedTwiceInconsistency
36 { bundle_id : String, leafname : String },
37}
38
39use Error::*;
40
41pub struct SourceGenerator {
42 output_dir : Immutable<String>,
43 config : Config,
44 items : BTreeMap<Category, Vec<OutputItem>>,
45 used_leaves : HashMap<String, String>,
46}
47
48pub struct Config {
49 include_upstream : bool,
50 cargo_source_cat : Box<dyn FnMut(&Option<Source>) -> Result<Category,E>>,
51 tidy : bool,
52}
53
54impl Default for Config {
55 fn default() -> Config {
56 let cargo_source_cat = Box::new(|s : &Option<Source>| Ok(
57 if s.as_ref().map_or(false, |s : &Source| s.is_crates_io())
58 { Upstream } else { Modified }
59 ));
60
61
62 Config {
63 include_upstream : true,
64 tidy : true,
65 cargo_source_cat,
66 }
67 }
68}
69
70impl Config {
71 pub fn new() -> Config { Default::default() }
72 pub fn include_upstream(&mut self, i : bool) -> &mut Self {
73 self.include_upstream = i; self
74 }
75 pub fn tidy(&mut self, i : bool) -> &mut Self {
76 self.tidy = i; self
77 }
78}
79
80#[derive(Debug,Clone,Serialize)]
81pub struct OutputItem {
82 pub leafname : String,
83 pub desc : Html,
84 cat : Category,
85}
86
87#[derive(Debug,Ord,PartialOrd,Eq,PartialEq,Copy,Clone,Hash)]
88#[derive(Serialize,Deserialize)]
89pub enum Category {
90 Upstream,
91 Mixed,
92 Modified,
93 Metadata,
94}
95use Category::*;
96
97impl SourceGenerator {
98 pub fn new_adding(output_dir : String) -> Result<SourceGenerator,E> {
100 let output_dir = output_dir.to_owned();
101 match create_dir(&output_dir) {
102 Ok(_) => (),
103 Err(e) if e.kind() == AlreadyExists => (),
104 e @ Err(_) => e.with_context(|| format!(
105 "new_adding: create output directory, if needed {:?}",
106 &output_dir
107 ))?
108 };
109 Ok( Self::new_return(output_dir) )
110 }
111
112 #[throws(E)]
113 pub fn new_trashing(output_dir : String) -> SourceGenerator {
114 let output_dir = output_dir.to_owned();
115 match remove_dir_all(&output_dir) {
116 Ok(_) => { },
117 Err(e) if e.kind() == NotFound => { },
118 e @ Err(_) =>
119 e.cxm(||format!(
120 "new_trashing: remove original output directory {:?}",
121 &output_dir))?,
122 }
123 create_dir(&output_dir).cxm(||format!(
124 "new_trashing: create new output directory {:?}",
125 &output_dir))?;
126 Self::new_return(output_dir)
127 }
128
129 fn new_return(output_dir : String) -> SourceGenerator {
130 let output_dir = output_dir.into();
131 let items = Default::default();
132 let used_leaves = HashMap::new();
133 let config = Config::new();
134 SourceGenerator { output_dir, items, used_leaves, config }
135 }
136 pub fn configure(&mut self, config : Config) -> &mut Self {
137 self.config = config;
138 self
139 }
140
141 fn output_file<T : AsRef<str>>(&self, sub : T) -> String {
142 self.output_dir.to_owned() + "/" + sub.as_ref()
143 }
144
145 #[throws(E)]
146 pub fn add_component<C : Component>(&mut self,
148 src : &C, basename : &str,
149 cat : Category, desc : Html)
150 {
151 let basename = basename.to_owned();
152 let baseleaf = basename; let bundle_id = format!("{:?} {:?}", cat, src);
154 if self.check_record_leaf_done(&baseleaf, &bundle_id)? { return }
155
156 let basepath = self.output_file(&baseleaf);
157 let suffix = src.bundle(&basepath)?;
158 let already_checked = suffix.is_none();
159
160 let suffix = suffix.as_ref().map(|s| s.as_ref()).unwrap_or("");
161 let fullleaf = baseleaf + suffix;
162 self.add_bundle(fullleaf, cat, desc, bundle_id, already_checked)?;
163 }
164
165 #[throws(Error)]
166 fn check_record_leaf_done(&mut self, leafname : &str, bundle_id : &str)
167 -> bool {
168 let leafname = leafname.to_owned();
169 let bundle_id = bundle_id.to_owned();
170 if leafname.len() == 0
171 || !leafname.starts_with(|c:char| c.is_ascii_lowercase())
172 || leafname.find('/').is_some()
173 || leafname.split('.').next().unwrap() == INDEX_BASENAME {
174 throw!(BadLeafBasename(leafname))
175 }
176 match self.used_leaves.entry(leafname) {
177 Vacant(e) => {
178 e.insert(bundle_id);
179 false
180 }
181 Occupied(e) if e.get() == &bundle_id => {
182 true
183 }
184 Occupied(e) => {
185 throw!(LeafReused{
186 leafname : e.key().clone(),
187 first : e.get().clone(),
188 second : bundle_id,
189 })
190 }
191 }
192 }
193
194 #[throws(Error)]
195 fn add_bundle(&mut self, fullleaf : String,
196 cat : Category, desc : Html, bundle_id : String,
197 already_checked: bool) {
198 if !already_checked {
199 let already = self.check_record_leaf_done(&fullleaf, &bundle_id)?;
200 if already { throw!(ComponentLeafGeneratedTwiceInconsistency{
201 bundle_id, leafname : fullleaf }) }
202 }
203 self.items.entry(cat).or_default().push(
204 OutputItem { leafname : fullleaf, desc, cat });
205 }
206
207 #[throws(E)]
208 pub fn add_cargo_packages(&mut self, spec_manifest_path : Option<&str>) {
210 let manifest_path = match spec_manifest_path
211 .map( |i| i.to_owned() )
212 {
213 Some(i) if i == "Cargo.toml" || i.ends_with("/Cargo.toml") => i,
214 Some(i) => { i + "/Cargo.toml" },
215 None => {
216 match env::var("CARGO_MANIFEST_DIR") {
217 Err(VarError::NotPresent) => "Cargo.toml".to_owned(),
218 e @ Err(_) => e.cxm(||"looking up CARGO_MANIFEST_DIR".to_owned())?,
219 Ok(d) => d + "/Cargo.toml",
220 }
221 }
222 };
223
224 #[throws(E)]
225 fn package_leafname(name : &str, version : &str) -> String {
226 let check = |s : &str, what|{
227 let badness = s.trim_matches(
228 |c : char| c=='.' || c=='_' || c=='-' || c=='+' ||
229 c.is_ascii_alphanumeric()
230 );
231 if badness.len() != 0 {
232 Err(anyhow!("package {:?} {} bad syntax ({:?})",
233 name, what, badness))?;
234 }
235 <Result<(),E>>::Ok(())
236 };
237 check(version,"version")?;
238 check(name,"name")?;
239 format!("{}={}", name, version)
240 }
241
242 let (metafile_leafname, metafile_desc) = if spec_manifest_path.is_none() {
243 ("cargo-metadata.json".to_owned(),
244 Html("Rust cargo metadatafile".to_owned()))
245 } else {
246 (||{
247 let manifest_data = fs::read_to_string(&manifest_path)?;
248 let manifest_data = manifest_data.parse()?;
249 let getkey
250 : for<'v> fn(&'v toml::value::Value,&'_ str) -> Result<&'v _,E>
251 = |d,k| {
252 d.get(k).ok_or_else(
253 ||anyhow!(r#"missing "{}" in Cargo.toml"#, k))
254 };
255 let getstr = |d, k| {
256 getkey(d,k)?.as_str().ok_or_else(
257 ||anyhow!(r#"value for "{}" is not a string"#, k))
258 };
259 let package = getkey(&manifest_data,"package")?;
260 let name = getstr(package,"name")?;
261 let version = getstr(package,"version")?;
262 let metafile_base = package_leafname(&name, &version)?;
263 <Result<_,E>>::Ok((
264 metafile_base + ".cargo-metadata.json",
265 Html(format!("Rust cargo metadatafile for package {}",
266 Html::from_literal(&name).0))
267 ))
268 })().cxm(||format!("read manifest {:?} to find package name and version",
269 manifest_path))?
270 };
271
272 let metafile_path = self.output_file(&metafile_leafname);
273
274 let mut cmd = MetadataCommand::new()
275 .manifest_path(&manifest_path)
276 .other_options(vec!["--offline".to_string()])
277 .other_options(vec!["--locked".to_string()])
278 .cargo_command();
279 cmd
280 .stdout(File::create(&metafile_path)?);
281 if let Some(cd) = env::var_os("CARGO_HOME") {
282 cmd.current_dir(cd);
283 }
284 let st = cmd
285 .status()?;
286
287 let bundle_id = format!("add_cargo_packages(manifest_path={:?})",
288 &manifest_path);
289
290 self.add_bundle(metafile_leafname, Metadata,
291 metafile_desc, bundle_id, false)?;
292
293 check_exit_status(st).context("cargo metadata failed")?;
294
295 let metadata = fs::read_to_string(metafile_path)
296 .context("cannot read metadata file")?;
297 let mut metadata = MetadataCommand::parse(&metadata)?;
298
299 metadata.packages.sort_by(|a,b|{
300 let gf : fn(&_) -> (&_,&_,&_) =
301 |p : &Package| (&p.name, &p.version, &p.id);
302 gf(a).cmp(&gf(b))
303 });
304 for package in &metadata.packages {
305 let pmf = &package.manifest_path;
306 let src = pmf.parent().ok_or_else(
307 || anyhow!("manifest path without directory: {:?}", &package)
308 )?.as_str();
309 let outleaf = package_leafname(&package.name,
310 &format!("{}",&package.version))?;
311 let desc = Html(format!(
312 "rust package {} version {}",
313 Html::from_literal(&package.name).0,
314 Html::from_literal(&format!("{}",package.version)).0));
315
316 let cat = (self.config.cargo_source_cat)(&package.source)?;
317 self.add_component(&DirectoryComponent::new(src.to_owned())?,
318 &outleaf, cat, desc)?;
319 }
320 }
321
322 #[throws(E)]
323 pub fn aggregate(mut self) -> Vec<OutputItem> {
324 let mut categories : Vec<_> = self.items.keys().map(|c| *c).collect();
325 categories.sort();
326 categories.reverse();
327
328 let mut results = vec![];
329 {
330 let include_upstream = self.config.include_upstream;
331 let mut agg = |mincat, nomcat, outleaf, desc |{
332 self.aggregate_min_category(&categories,&mut results,
333 mincat,nomcat,outleaf,desc)
334 .cxm(||format!("failure aggregating {}", outleaf))
335 };
336 agg(Mixed, Modified, "Modified",
337 &HtmlRef("Sources, locally modified"))?;
338 if include_upstream {
339 agg(Upstream, Mixed, "Comprehensive",
340 &HtmlRef("All sources"))?;
341 }
342 }
343
344 let mut index = IndexWriter::new(&self.output_dir,
345 &HtmlRef("Master index"))?;
346 {
347 let mut table = index.table(Html("Source code bundles".to_owned()))?;
348
349 for &OutputItem { leafname : ref tarfile, ref desc, .. } in &results {
350 table.entry(tarfile, &desc.as_ref())?;
351 }
352 }
353 index.finish()?;
354
355 if self.config.tidy {
356 for OutputItem { ref leafname, .. } in self.items.values().flatten() {
357 remove_file(self.output_file(leafname)).cxm(||format!(
358 "failed to remove {:?} while tidying up", leafname
359 ))?;
360 }
361 }
362
363 (||{
364 let mut f = File::create(self.output_file("index.json"))
365 .context("create output file")?;
366 writeln!(f,"{}",&serde_json::to_string(&results)?)?;
367 f.flush()?;
368 <Result<(),E>>::Ok(())
369 })().context("create index.json file")?;
370
371 let mut output = vec![ OutputItem{
372 leafname : INDEX.to_owned(),
373 cat : Metadata,
374 desc : Html("index of output bundles".to_owned())
375 }];
376 output.extend(results);
377 output
378 }
379
380 #[throws(E)]
381 fn aggregate_min_category(&mut self, cats : &Vec<Category>,
382 results : &mut Vec<OutputItem>,
383 mincat : Category, nomcat : Category,
384 outleafbase : &str, desc : &HtmlRef) {
385 let tarfile = outleafbase.to_string() + ".tar.gz";
386
387 let linkpath = self.output_file(&outleafbase);
388 remove_file(&linkpath).or_else(
389 |e| if e.kind() == NotFound { Ok(()) } else { Err(e) })?;
390 symlink(".", &linkpath)?;
391
392 let mut leaves = vec![ INDEX.to_owned() ];
393 let mut index = IndexWriter::new(&self.output_dir,desc)?;
394
395 for &cat in cats {
396 let mut table = index.table(match cat {
397 Metadata => HtmlRef("Metadata and index files"),
398 Modified => HtmlRef("Local sources, possibly modified"),
399 Upstream => HtmlRef("Upstream sources included for completeness"),
400 Mixed => HtmlRef("Mixed, other or unknown content"),
401 }.to_owned())?;
402 for &OutputItem { ref leafname, cat, ref desc } in &self.items[&cat] {
403 if cat < mincat { continue }
404 leaves.push(leafname.to_owned());
405 table.entry(&leafname, &desc.as_ref())?;
406 }
407 }
408 index.finish()?;
409
410 let mut cmd = tar_command();
411 cmd
412 .current_dir(&*self.output_dir)
413 .args(&["--use-compress-program=gzip --rsyncable",
414 "--no-recursion",
415 "-cf", &tarfile, "--"]);
416 cmd
417 .args( leaves.drain(0..).map(
418 |l| outleafbase.to_owned() + "/" + &l
419 ) );
420
421 run_cmd(cmd).cxm(||format!("build aggregate (tar) {:?}", &desc))?;
422
423 remove_file(linkpath)?;
424
425 results.push(OutputItem {
426 leafname : tarfile,
427 desc : desc.to_owned(),
428 cat : nomcat,
429 });
430 }
431}
432
433#[cfg(test)] use tempfile::TempDir;
434
435#[test]
436fn selftest() -> Result<(),E> {
437let output_dir = env::var_os("BUNDLE_RUST_SOURCES_TEST_OUTPUT");
440 let mut _drop_temp = None;
441 let output_dir = match output_dir {
442 None => {
443 _drop_temp = Some(TempDir::new()?);
444 _drop_temp.as_ref().unwrap().path().as_os_str()
445 }
446 Some(ref output_dir) => output_dir,
447 };
448 let output_dir = output_dir.to_str().ok_or_else(
449 || anyhow!("tempdir to_str!")
450 )?;
451 let mut sg = SourceGenerator::new_adding(output_dir.to_string())?;
452 sg.add_cargo_packages(None)?;
453 let _r = sg.aggregate()?;
454 Ok(())
455}