├── doc ├── requirements.txt ├── config │ ├── index.rst │ ├── metadata.rst │ ├── overview.rst │ ├── repositories.rst │ └── versions.rst ├── index.rst ├── qa.rst ├── version.rst ├── Makefile └── conf.py ├── .gitignore ├── src ├── re.rs ├── pack │ ├── deb.rs │ ├── ar.rs │ ├── tar.rs │ └── mod.rs ├── deb_ext.rs ├── hash_file.rs ├── bulk_version.rs ├── repo │ ├── deb.rs │ ├── ar.rs │ ├── metadata.rs │ ├── mod.rs │ └── debian.rs ├── config.rs ├── ver │ ├── scanner.rs │ ├── bump.rs │ ├── commit.rs │ └── mod.rs ├── main.rs └── version.rs ├── tests └── deb.rs ├── Cargo.toml ├── LICENSE-MIT ├── bulk.yaml ├── .travis.yml ├── README.rst ├── vagga.yaml ├── LICENSE-APACHE └── Cargo.lock /doc/requirements.txt: -------------------------------------------------------------------------------- 1 | sphinx_rtd_theme 2 | sphinxcontrib-domaintools==0.1 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.vagga 2 | /target 3 | /pkg 4 | /dist 5 | /doc/_build 6 | 7 | 8 | -------------------------------------------------------------------------------- /doc/config/index.rst: -------------------------------------------------------------------------------- 1 | Configuring Bulk 2 | ================ 3 | 4 | Contents: 5 | 6 | .. toctree:: 7 | :maxdepth: 1 8 | 9 | overview 10 | versions 11 | metadata 12 | repositories 13 | 14 | 15 | -------------------------------------------------------------------------------- /doc/index.rst: -------------------------------------------------------------------------------- 1 | .. Bulk documentation master file, created by 2 | sphinx-quickstart on Fri Jun 10 19:30:43 2016. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | Welcome to Bulk's documentation! 7 | ================================ 8 | 9 | Contents: 10 | 11 | .. toctree:: 12 | :maxdepth: 2 13 | 14 | config/index 15 | version 16 | qa 17 | 18 | 19 | 20 | Indices and tables 21 | ================== 22 | 23 | * :ref:`genindex` 24 | * :ref:`search` 25 | 26 | -------------------------------------------------------------------------------- /src/re.rs: -------------------------------------------------------------------------------- 1 | use std::sync::{Arc, Mutex}; 2 | use std::collections::HashMap; 3 | 4 | pub use regex::{Regex, Error, Captures}; 5 | 6 | 7 | lazy_static! { 8 | static ref CACHE: Mutex>> = 9 | Mutex::new(HashMap::new()); 10 | } 11 | 12 | pub fn compile<'x, S: AsRef>(x: S) -> Result, Error> { 13 | let s = x.as_ref(); 14 | if let Some(r) = CACHE.lock().unwrap().get(s) { 15 | return Ok(r.clone()); 16 | } 17 | Regex::new(s).map(|compiled| { 18 | let arc = Arc::new(compiled); 19 | CACHE.lock().unwrap().insert(s.to_string(), arc.clone()); 20 | arc 21 | }) 22 | } 23 | -------------------------------------------------------------------------------- /src/pack/deb.rs: -------------------------------------------------------------------------------- 1 | use std::io::{self, Write}; 2 | 3 | use config::Metadata; 4 | use deb_ext::WriteDebExt; 5 | 6 | 7 | pub fn format_deb_control(out: &mut W, meta: &Metadata, 8 | version: &str, architecture: &str) 9 | -> io::Result<()> 10 | { 11 | try!(out.write_kv("Package", &meta.name)); 12 | try!(out.write_kv("Version", &version)); 13 | try!(out.write_kv("Maintainer", "bulk")); 14 | try!(out.write_kv("Architecture", architecture)); 15 | if let Some(ref deps) = meta.depends { 16 | try!(out.write_kv("Depends", deps)); 17 | } 18 | try!(out.write_kv("Description", 19 | &format!("{}\n{}", meta.short_description, meta.long_description))); 20 | Ok(()) 21 | } 22 | -------------------------------------------------------------------------------- /tests/deb.rs: -------------------------------------------------------------------------------- 1 | extern crate tempfile; 2 | extern crate assert_cli; 3 | 4 | use std::fs::{create_dir, write}; 5 | 6 | #[test] 7 | fn long_file() { 8 | let dir = tempfile::tempdir().unwrap(); 9 | let pkg = dir.path().join("pkg"); 10 | create_dir(&pkg).unwrap(); 11 | let dist = dir.path().join("dist"); 12 | create_dir(&dist).unwrap(); 13 | write(pkg.join( 14 | "0123456789012345678901234567890123456789012345678901234567890\ 15 | 12345678901234567890123456789012345678901234567890123456789"), 16 | "hello-long-file").unwrap(); 17 | assert_cli::Assert::main_binary() 18 | .with_args(&["pack", "--dir"]) 19 | .with_args(&[&pkg]) 20 | .with_args(&["--dest-dir"]) 21 | .with_args(&[&dist]) 22 | .stderr().satisfies(|x| x.len() == 0, "bad output") 23 | .unwrap(); 24 | } 25 | -------------------------------------------------------------------------------- /src/deb_ext.rs: -------------------------------------------------------------------------------- 1 | use std::io::{self, Write}; 2 | 3 | 4 | pub trait WriteDebExt: Write { 5 | fn write_kv(&mut self, key: &str, value: &str) -> io::Result<()> { 6 | write!(self, "{}: {}\n", key, _control_multiline(value)) 7 | } 8 | fn write_kv_lines(&mut self, key: &str, lines: I) -> io::Result<()> 9 | where I: Iterator, E: AsRef 10 | { 11 | try!(write!(self, "{}:\n", key)); 12 | for line in lines { 13 | try!(write!(self, " {}\n", _control_multiline(line.as_ref()))); 14 | } 15 | Ok(()) 16 | } 17 | } 18 | 19 | fn _control_multiline(val: &str) -> String { 20 | let mut val = val 21 | .replace("\n\n", "\n.\n") 22 | .replace("\n", "\n "); 23 | let trimmed_len = val.trim_right().len(); 24 | val.truncate(trimmed_len); 25 | return val; 26 | } 27 | 28 | impl WriteDebExt for T {} 29 | -------------------------------------------------------------------------------- /src/hash_file.rs: -------------------------------------------------------------------------------- 1 | use std::io::{self, Read}; 2 | use std::fs::File; 3 | use std::path::Path; 4 | 5 | use sha2::{Sha256, Digest}; 6 | 7 | 8 | pub fn hash_stream(hash: &mut D, reader: &mut R) 9 | -> Result<(), io::Error> 10 | { 11 | let mut buf = [0u8; 8*1024]; 12 | loop { 13 | let len = match reader.read(&mut buf[..]) { 14 | Ok(0) => break, 15 | Ok(len) => len, 16 | Err(ref e) if e.kind() == io::ErrorKind::Interrupted => continue, 17 | Err(e) => return Err(e), 18 | }; 19 | hash.input(&buf[..len]); 20 | } 21 | Ok(()) 22 | } 23 | 24 | pub fn hash_file>(filename: F) -> io::Result { 25 | let mut sha256 = Sha256::new(); 26 | let mut file = try!(File::open(filename.as_ref())); 27 | try!(hash_stream(&mut sha256, &mut file)); 28 | return Ok(format!("{:x}", sha256.result())); 29 | } 30 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "bulk" 3 | version = "0.4.12" 4 | authors = ["Paul Colomiets"] 5 | license = "MIT" 6 | repository = "http://github.com/tailhook/bulk" 7 | description = """ 8 | A simple tool for making deb packages, repositories, 9 | and update version numbers. 10 | """ 11 | 12 | [dependencies] 13 | tar = "0.4.14" 14 | scan_dir = "0.3.2" 15 | quire = "0.4.0" 16 | argparse = "0.2" 17 | serde = "1.0.0" 18 | serde_derive = "1.0.27" 19 | libflate = "0.1.13" 20 | regex = "1.0.0" 21 | lazy_static = "1.0.0" 22 | unicase = "2.1.0" 23 | log = "0.4.1" 24 | env_logger = "0.5.5" 25 | sha2 = "0.7.0" 26 | time = "0.1.39" 27 | matches = "0.1.2" 28 | quick-error = "1.1.0" 29 | git2 = { version="0.7.0", default-features=false } 30 | tempfile = "3.0.2" 31 | failure = "0.1.1" 32 | 33 | [dev-dependencies] 34 | assert_cli = "0.6.2" 35 | 36 | [[bin]] 37 | name = "bulk" 38 | path = "src/main.rs" 39 | 40 | [profile.release] 41 | debug = true 42 | lto = true 43 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016 The bulk Developers 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /doc/config/metadata.rst: -------------------------------------------------------------------------------- 1 | Package Metadata 2 | ================ 3 | 4 | Package information is stored in ``metadata`` section in ``bulk.yaml``. 5 | Here is an example: 6 | 7 | .. code-block:: yaml 8 | 9 | metadata: 10 | name: your-app 11 | short-description: A great app in python 12 | long-description: 13 | A very great app in python. You can use it to do 14 | amazing things 15 | depends: [python3] 16 | 17 | 18 | Options: 19 | 20 | ``name`` 21 | Package name used to name a ``.deb`` file 22 | 23 | ``short-description`` 24 | Short description of the package as shown in package search results and in 25 | other places. It should be a one-liner 26 | 27 | ``long-description`` 28 | Long description of the package. Usually shown in GUI tools as a part of 29 | package detail. 30 | 31 | ``depends`` 32 | List of package dependencies. It can consists of any expression allowed in 33 | debian packages. But note if you need different dependencies for different 34 | packages built (i.e. for different ubuntu distributions) you need to use 35 | different ``bulk.yaml`` configs and specify ones explicity to ``bulk pack``. 36 | -------------------------------------------------------------------------------- /src/bulk_version.rs: -------------------------------------------------------------------------------- 1 | use quire::ast::{Ast, Tag}; 2 | use quire::{Error as QuireError, ErrorCollector}; 3 | use quire::validate as V; 4 | 5 | use version::Version; 6 | 7 | #[derive(Debug)] 8 | pub struct MinimumVersion(pub Version<&'static str>); 9 | 10 | impl V::Validator for MinimumVersion { 11 | fn default(&self, _pos: V::Pos) -> Option { 12 | None 13 | } 14 | fn validate(&self, ast: Ast, errors: &ErrorCollector) -> Ast { 15 | let (pos, kind, val) = match ast { 16 | Ast::Scalar(pos, _, kind, min_version) => { 17 | if Version(&min_version[..]) > self.0 { 18 | errors.add_error(QuireError::validation_error(&pos, 19 | format!("This package configure requires bulk \ 20 | of at least {:?}", min_version))); 21 | } 22 | (pos, kind, min_version) 23 | } 24 | ast => { 25 | errors.add_error(QuireError::validation_error(&ast.pos(), 26 | format!("Value of bulk-version must be scalar"))); 27 | return ast; 28 | } 29 | }; 30 | return Ast::Scalar(pos, Tag::NonSpecific, kind, val) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /bulk.yaml: -------------------------------------------------------------------------------- 1 | minimum-bulk: v0.4.5 2 | 3 | metadata: 4 | name: bulk 5 | short-description: A package builder for multiple linux distributions 6 | long-description: 7 | A single tool for creating packages for multiple linux distributions. The 8 | tool favors simplicity over ubiquity and is intended for use for 9 | containers. So it lacks some rarely useful features like install scripts. 10 | 11 | repositories: 12 | 13 | - kind: debian 14 | suite: static 15 | component: bulk 16 | keep-releases: 1 17 | match-version: ^\d+\.\d+\.\d+$ 18 | add-empty-i386-repo: true 19 | 20 | - kind: debian 21 | suite: static 22 | component: bulk-stable 23 | keep-releases: 1000 24 | match-version: ^\d+\.\d+\.\d+$ 25 | add-empty-i386-repo: true 26 | 27 | - kind: debian 28 | suite: static 29 | component: bulk-testing 30 | keep-releases: 100 31 | add-empty-i386-repo: true 32 | 33 | versions: 34 | 35 | - file: Cargo.toml 36 | block-start: ^\[package\] 37 | block-end: ^\[.*\] 38 | regex: ^version\s*=\s*"(\S+)" 39 | 40 | - file: Cargo.lock 41 | block-start: ^name\s*=\s*"bulk" 42 | regex: ^version\s*=\s*"(\S+)" 43 | block-end: ^\[.*\] 44 | 45 | - file: README.rst 46 | regex: bulk-(\S+)\.tar\.gz 47 | 48 | #- file: bulk.yaml 49 | # regex: ^minimum-bulk:\s+v(\S+) 50 | -------------------------------------------------------------------------------- /doc/qa.rst: -------------------------------------------------------------------------------- 1 | Q & A 2 | ===== 3 | 4 | 5 | Why version number and file existence is optional? 6 | -------------------------------------------------- 7 | 8 | Sometimes we want to put and edit version number in generated files: 9 | lock-files, code generated things and other. 10 | 11 | Since entries in ``bulk.yaml`` are almost never modified it's much easier to 12 | check once after editing a file than to learn rules of what is strict and what 13 | isn't. 14 | 15 | Here is just one example, when it is useful. Here is how we configure bulk in 16 | rust projects: 17 | 18 | .. code-block:: toml 19 | 20 | - file: Cargo.toml 21 | block-start: ^\[package\] 22 | block-end: ^\[.*\] 23 | regex: ^version\s*=\s*"(\S+)" 24 | 25 | - file: Cargo.lock 26 | block-start: ^name\s*=\s*"project-name" 27 | regex: ^version\s*=\s*"(\S+)" 28 | block-end: ^\[.*\] 29 | 30 | The important part is that we *must* update ``Cargo.lock`` so that 31 | ``bulk set-version/incr-version/bump -g`` works fine *(we modify 32 | ``Cargo.lock`` together with ``Cargo.toml`` and commit in the same commit, 33 | if we don't do that lockfile is update on next build and needs to be commited 34 | after)*. 35 | 36 | But we also want to be able to run bulk with absent lockfile (in case we don't 37 | commit it into a repository) or if we want cargo to rebuild it from scratch. 38 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: rust 2 | rust: stable 3 | os: linux 4 | dist: trusty 5 | sudo: false 6 | addons: 7 | apt: 8 | packages: 9 | - fakeroot 10 | - musl-tools 11 | 12 | cache: 13 | - apt 14 | - cargo 15 | 16 | install: 17 | - rustup target add x86_64-unknown-linux-musl 18 | 19 | script: 20 | - cargo test --verbose $CARGO_ARGS 21 | - cargo build --target=x86_64-unknown-linux-musl --release 22 | 23 | before_deploy: | 24 | mkdir -p dist 25 | fakeroot sh -ecx ' 26 | install -D target/x86_64-unknown-linux-musl/release/bulk pkg/usr/bin/bulk 27 | tar -C pkg -czf dist/bulk-$TRAVIS_TAG.tar.gz usr 28 | ' 29 | 30 | deploy: 31 | provider: releases 32 | api_key: 33 | secure: "mYZ+nwYRqW2FWxWS5pwvZLpQIrNdzh2FXwSsECQtDMXsulqO52oMZgldH42l6fcYq8cEDqrwRifo8G3Z4xYXIFdCPzrDej/Uqt8ri976fbnAuWg965KMA6CnzVANCiYlVekp/R1su/vPx8b1hjO/RpXfb76cv4oHMH63ZKFArb9aqPHz7AUthMpuWqKCYvNb8/pSWwxLDB9rIeED/uLJe6F3ysJYw0uQWQpmbep6363b/m9/9K1wcbMfyrL418WoaatVyCNXLibK8g9+12vhidR631VOX4iVzYCRmTmK6wvXrHWY+k9cGvmttTtMFjBJJfGTNElbQ22HjEAxIxRn+1tO7X8E70qRRchmzvbs9srpEO1L1qsl+F2PZJOhCW5DYopt8B50dFsWzT+fnsToJzy/U5RosMPcqdSb79uIHN8dbJtSf5oQFJw8qEF6PxlnW5gTK28+74v2Re8ebv1l+vGueswdsJq9WCXCkgXW0w19TwrXDqZnL0X1mFXPYbk9SK4x2WCI8QV9Twd/ykrLm+aPHbrHAqQbICdeH3XHvlfMhQ+DR039Of0ayRvb2Ty/713ontJ2a54fWOW9bG62NnJyDe8dTgVmVki7LkAkxIe3nbkkCZ4XNQIVLgbP7lEJfi3QSOEnezDTRUQH/GLX/QeNHy6S1qaa671Vhu37nYY=" 34 | file: "dist/bulk-$TRAVIS_TAG.tar.gz" 35 | skip_cleanup: true 36 | on: 37 | tags: true 38 | -------------------------------------------------------------------------------- /doc/config/overview.rst: -------------------------------------------------------------------------------- 1 | Overview 2 | ======== 3 | 4 | Bulk's configuration file is usually ``bulk.yaml`` in the root if your project 5 | but can be overrident by ``-c`` or ``--config`` for most subcommands. 6 | 7 | This is yaml config parsed by quire_ so you can use most of its features 8 | there. 9 | 10 | .. _quire: http://quire.readthedocs.io/ 11 | 12 | Configuration file consists of a declaration of minimum supported version 13 | of bulk and three sections, here is an example: 14 | 15 | .. code-block:: yaml 16 | 17 | minimum-bulk: v0.4.5 18 | 19 | versions: 20 | - file: setup.py 21 | regex: ^\s*version\s*=\s*["']([^"']+)["'] 22 | - file: your_module/__init__.py 23 | regex: ^__version__\s*=\s*["']([^"']+)["'] 24 | 25 | metadata: 26 | name: your-app 27 | short-description: A great app in python 28 | long-description: 29 | A very great app in python 30 | 31 | repositories: 32 | - kind: debian 33 | suite: bionic 34 | component: your-app 35 | 36 | All sections are optional if you don't want to use some of the functionality 37 | here. In particular: 38 | 39 | 1. ``versions`` needed if you want to keep project version in source code in 40 | multiple places and want to update it using bulk 41 | 2. ``metadata`` is a package metadata, if you don't build ``.deb`` package 42 | you don't need it 43 | 3. ``repositories`` is a repository metadata, if you don't have debian/ubuntu 44 | repository you don't need it. 45 | 46 | -------------------------------------------------------------------------------- /src/repo/deb.rs: -------------------------------------------------------------------------------- 1 | use std::io::{self, Read, BufRead, BufReader}; 2 | use std::mem::replace; 3 | use std::collections::HashMap; 4 | 5 | use unicase::UniCase; 6 | 7 | 8 | fn error(text: &'static str) -> io::Error { 9 | return io::Error::new(io::ErrorKind::Other, text); 10 | } 11 | 12 | 13 | pub fn parse_control(r: R) 14 | -> io::Result, String>>> 15 | { 16 | let src = BufReader::new(r); 17 | let mut res = Vec::new(); 18 | let mut current_hash = HashMap::new(); 19 | let mut buf = None::<(String, String)>; 20 | for line in src.lines() { 21 | let line = try!(line); 22 | if line.len() == 0 { 23 | if let Some((key, val)) = buf.take() { 24 | current_hash.insert(UniCase::new(key), val); 25 | } 26 | if current_hash.len() > 0 { 27 | res.push(replace(&mut current_hash, HashMap::new())); 28 | } 29 | } else if line.starts_with(' ') { 30 | if let Some((_, ref mut val)) = buf { 31 | val.push_str("\n"); 32 | if line != " ." { 33 | val.push_str(&line[1..]); 34 | } 35 | } else { 36 | return Err(error("Bad format of debian control")); 37 | } 38 | } else { 39 | if let Some((key, val)) = buf.take() { 40 | current_hash.insert(UniCase::new(key), val); 41 | } 42 | let mut pair = line.splitn(2, ':'); 43 | match (pair.next(), pair.next()) { 44 | (Some(k), Some(v)) => { 45 | buf = Some((k.to_string(), v.trim().to_string())); 46 | } 47 | _ => return Err(error("Bad format of debian control")), 48 | } 49 | } 50 | } 51 | if let Some((key, val)) = buf.take() { 52 | current_hash.insert(UniCase::new(key), val); 53 | } 54 | if current_hash.len() > 0 { 55 | res.push(current_hash); 56 | } 57 | return Ok(res); 58 | } 59 | -------------------------------------------------------------------------------- /src/repo/ar.rs: -------------------------------------------------------------------------------- 1 | use std::io::{self, Read, Take}; 2 | use std::str; 3 | use std::path::Path; 4 | use std::os::unix::ffi::OsStrExt; 5 | 6 | pub struct Archive(T, bool); 7 | 8 | 9 | fn error(text: &'static str) -> io::Error { 10 | return io::Error::new(io::ErrorKind::Other, text); 11 | } 12 | 13 | 14 | impl Archive { 15 | pub fn new(mut stream: T) -> io::Result> { 16 | let mut sig = [0u8; 8]; 17 | try!(stream.read(&mut sig)); 18 | if &sig != b"!\n" { 19 | return Err(error("Archive signature is wrong")); 20 | } 21 | Ok(Archive(stream, false)) 22 | } 23 | /// Reads file with known name 24 | /// 25 | /// Since we only read debian archives, it's good enough 26 | pub fn read_file<'x, P: AsRef>(&mut self, name: P) 27 | -> io::Result> 28 | { 29 | return self._read_file(name.as_ref()); 30 | } 31 | fn _read_file<'x>(&mut self, name: &Path) -> io::Result> { 32 | let mut buf = [0u8; 61]; 33 | let head = { 34 | if self.1 { 35 | let bytes = try!(self.0.read(&mut buf[..61])); 36 | if bytes != 61 { 37 | return Err(error("Premature end of file")); 38 | } 39 | &buf[1..60] 40 | } else { 41 | let bytes = try!(self.0.read(&mut buf[..60])); 42 | if bytes != 60 { 43 | return Err(error("Premature end of file")); 44 | } 45 | &buf[0..60] 46 | } 47 | }; 48 | if &head[58..60] != b"`\n" { 49 | return Err(error("Invalid file format")); 50 | } 51 | let fnameend = head[..16].iter().position(|&x| x == b' ') 52 | .unwrap_or(16); 53 | if &head[..fnameend] != name.as_os_str().as_bytes() { 54 | return Err(error("Unexpected archive member")); 55 | } 56 | let size = try!( 57 | str::from_utf8(&head[48..58]).ok() 58 | .and_then(|x| x.trim().parse().ok()) 59 | .ok_or_else(|| error("Invalid file size"))); 60 | self.1 = size % 1 == 1; 61 | Ok((&mut self.0).take(size)) 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/repo/metadata.rs: -------------------------------------------------------------------------------- 1 | use std::io::{self, Read, BufReader}; 2 | use std::fs; 3 | use std::path::{Path, PathBuf}; 4 | use std::collections::HashMap; 5 | 6 | use libflate::gzip; 7 | use unicase::UniCase; 8 | 9 | use repo::ar; 10 | use repo::deb; 11 | use tar; 12 | 13 | #[derive(Debug)] 14 | pub struct PackageMeta { 15 | pub filename: PathBuf, 16 | pub name: String, 17 | pub arch: String, 18 | pub version: String, 19 | pub info: HashMap, String>, 20 | } 21 | 22 | fn error(text: &'static str) -> io::Error { 23 | return io::Error::new(io::ErrorKind::Other, text); 24 | } 25 | 26 | pub fn gather_metadata>(p: P) -> io::Result { 27 | let path = p.as_ref(); 28 | let buf = BufReader::new(fs::File::open(path)?); 29 | let mut arch = ar::Archive::new(buf)?; 30 | { 31 | let mut buf = String::with_capacity(4); 32 | let mut member = try!(arch.read_file("debian-binary")); 33 | try!(member.read_to_string(&mut buf)); 34 | if buf.trim() != "2.0" { 35 | return Err(error("Unsupported deb format")); 36 | } 37 | } 38 | let member = try!(arch.read_file("control.tar.gz")); 39 | let mut arch = tar::Archive::new(try!(gzip::Decoder::new(member))); 40 | for entry in try!(arch.entries()) { 41 | let entry = try!(entry); 42 | if try!(entry.path()) == Path::new("control") { 43 | let control = try!(deb::parse_control(entry)); 44 | if control.len() != 1 { 45 | return Err(error("Wrong control file in package")); 46 | } 47 | let hash = control.into_iter().next().unwrap(); 48 | return Ok(PackageMeta { 49 | filename: path.to_path_buf(), 50 | name: try!(hash.get(&"Package".into()).map(Clone::clone) 51 | .ok_or(error("No package name in deb package meta"))), 52 | arch: try!(hash.get(&"Architecture".into()).map(Clone::clone) 53 | .ok_or(error("No architecture in deb package meta"))), 54 | version: try!(hash.get(&"Version".into()).map(Clone::clone) 55 | .ok_or(error("No version in deb package meta"))), 56 | info: hash, 57 | }); 58 | } 59 | } 60 | return Err(error("No metadata found")); 61 | } 62 | -------------------------------------------------------------------------------- /src/pack/ar.rs: -------------------------------------------------------------------------------- 1 | use std::io; 2 | use std::io::{Write, Seek, SeekFrom}; 3 | 4 | 5 | pub const SIZE_AUTO: u64 = 9999999999; 6 | 7 | 8 | pub struct ArArchive(T); 9 | 10 | pub struct ArMember<'a, T:Write+Seek+'a> { 11 | position: u64, 12 | current_size: u64, 13 | defined_size: u64, 14 | archive: &'a mut ArArchive, 15 | } 16 | 17 | impl<'a, T:Write+Seek+'a> Write for ArMember<'a, T> { 18 | fn write(&mut self, data: &[u8]) -> Result { 19 | match self.archive.0.write(data) { 20 | Ok(x) => { 21 | self.current_size += x as u64; 22 | Ok(x) 23 | } 24 | Err(e) => Err(e), 25 | } 26 | } 27 | fn flush(&mut self) -> Result<(), io::Error> { 28 | self.archive.0.flush() 29 | } 30 | } 31 | 32 | impl<'a, T:Write+Seek+'a> Drop for ArMember<'a, T> { 33 | fn drop(&mut self) { 34 | if self.current_size != self.defined_size { 35 | // Since we had already written there, we assume that we can't 36 | // fail. Anyway crashing is probably okay in this case 37 | let cur_pos = self.archive.0.seek(SeekFrom::Current(0)).unwrap(); 38 | self.archive.0.seek(SeekFrom::Start(self.position + 48)).unwrap(); 39 | write!(self.archive.0, 40 | "{size:<10}", size=self.current_size).unwrap(); 41 | self.archive.0.seek(SeekFrom::Start(cur_pos)).unwrap(); 42 | } 43 | if self.current_size % 2 != 0 { 44 | self.archive.0.write(b"\n").unwrap(); 45 | } 46 | } 47 | } 48 | 49 | impl ArArchive { 50 | pub fn new(mut file: T) -> Result, io::Error> { 51 | try!(file.write_all(b"!\n")); 52 | Ok(ArArchive(file)) 53 | } 54 | pub fn add<'x>(&'x mut self, filename: &str, 55 | filemtime: u32, uid: u32, gid: u32, 56 | mode: u32, size: u64) -> Result, io::Error> 57 | { 58 | assert!(filename.len() <= 16); 59 | assert!(uid <= 999999); 60 | assert!(gid <= 999999); 61 | assert!(mode <= 99999999); 62 | assert!(size <= 9999999999); 63 | let pos = try!(self.0.seek(SeekFrom::Current(0))); 64 | try!(write!(&mut self.0, 65 | "{name:<16}{mtime:<12}{uid:<6}{gid:<6}{mode:<8o}{size:<10}`\n", 66 | name=filename, mtime=filemtime, uid=uid, gid=gid, mode=mode, 67 | size=size)); 68 | Ok(ArMember { 69 | position: pos, 70 | current_size: 0, 71 | defined_size: size, 72 | archive: self, 73 | }) 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/pack/tar.rs: -------------------------------------------------------------------------------- 1 | use std::io; 2 | use std::fs::{File, symlink_metadata, read_link}; 3 | use std::path::Path; 4 | use std::os::unix::fs::PermissionsExt; 5 | 6 | use tar; 7 | 8 | 9 | pub trait ArchiveExt { 10 | fn append_blob>(&mut self, name: P, mtime: u32, data: &[u8]) 11 | -> Result<(), io::Error>; 12 | fn append_file_at, Q: AsRef>(&mut self, 13 | dir: P, path: Q, mtime: u32) 14 | -> Result<(), io::Error>; 15 | } 16 | 17 | impl ArchiveExt for tar::Builder { 18 | fn append_blob>(&mut self, name: P, mtime: u32, data: &[u8]) 19 | -> Result<(), io::Error> 20 | { 21 | let mut head = tar::Header::new_gnu(); 22 | head.set_mtime(mtime as u64); 23 | head.set_size(data.len() as u64); 24 | head.set_mode(0o644); 25 | head.set_cksum(); 26 | self.append_data(&mut head, name, &mut io::Cursor::new(&data)) 27 | } 28 | /// This does same as Builder::append_file, but has no mtime/size/owner 29 | /// information which we explicitly have chosen to omit 30 | /// 31 | /// Silently skips things that are neither files nor symlinks 32 | fn append_file_at, Q: AsRef>(&mut self, 33 | dir: P, path: Q, mtime: u32) 34 | -> Result<(), io::Error> 35 | { 36 | let path = path.as_ref(); 37 | let fullpath = dir.as_ref().join(path); 38 | let meta = try!(symlink_metadata(&fullpath)); 39 | 40 | let mut head = tar::Header::new_gnu(); 41 | head.set_mtime(mtime as u64); 42 | 43 | if meta.file_type().is_file() { 44 | head.set_entry_type(tar::EntryType::Regular); 45 | let mut file = try!(File::open(&fullpath)); 46 | head.set_size(meta.len() as u64); 47 | head.set_mode(meta.permissions().mode()); 48 | head.set_cksum(); 49 | self.append_data(&mut head, &path, &mut file) 50 | } else if meta.file_type().is_symlink() { 51 | head.set_entry_type(tar::EntryType::Symlink); 52 | let lnk = try!(read_link(&fullpath)); 53 | head.set_size(0); 54 | head.set_mode(meta.permissions().mode()); 55 | try!(head.set_link_name(lnk)); 56 | head.set_cksum(); 57 | self.append_data(&mut head, &path, &mut io::empty()) 58 | } else if meta.file_type().is_dir() { 59 | head.set_entry_type(tar::EntryType::Directory); 60 | head.set_size(0); 61 | head.set_mode(meta.permissions().mode()); 62 | head.set_cksum(); 63 | self.append_data(&mut head, &path, &mut io::empty()) 64 | } else { 65 | // Silently skip as documented 66 | Ok(()) 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /doc/config/repositories.rst: -------------------------------------------------------------------------------- 1 | Repositories 2 | ============ 3 | 4 | When using bulk it's common to track multiple repositories using single 5 | config. Here is an example: 6 | 7 | .. code-block:: yaml 8 | 9 | repositories: 10 | 11 | - kind: debian 12 | suite: bionic 13 | component: your-app-testing 14 | keep-releases: 1000 15 | 16 | - kind: debian 17 | suite: bionic 18 | component: your-app 19 | keep-releases: 1 20 | match-version: ^\d+\.\d+\.\d+$ 21 | 22 | This keeps 1000 releases in testing repository. And just one release in 23 | stable repository. Where stable release has strict semantic version and 24 | all releases are included in testing repository (including stable). 25 | Non-stable releases themeselves are probably versioned with ``git describe`` 26 | yielding versions like this: ``1.2.3-34-gde103b3``. 27 | 28 | Options: 29 | 30 | ``kind`` 31 | Kind of the repository. Only ``debian`` is currently supported. 32 | 33 | ``suite`` 34 | Suite of the repository. For ubuntu it's usually a release codename such 35 | as ``xenial`` or ``bionic``. 36 | 37 | ``component`` 38 | Component of the repository. Common convention is that it's a application 39 | name (so technically you can put multiple applications in the same 40 | repository). Also it may include modifier like ``-testing`` or ``-stable``. 41 | 42 | ``keep-releases`` 43 | Number of releases of the package to keep in this repository. By default 44 | all releases are kept (i.e. it's never cleaned up). Usual debian tools 45 | keep exactly one package. 46 | 47 | It's also a good idea to keep two repositories: ``your-app`` with 48 | ``keep-releases: 1`` and ``your-app-stable`` with ``keep-releases: 100`` 49 | which keep older packages. The index of the first repository is smaller 50 | and faster to download and the latter can be used to downgrade. Note: 51 | repositories share a pool of packages so ``.deb`` file itself isn't 52 | duplicated for two repositories. 53 | 54 | ``match-version`` 55 | Only add version matching this regex to the repository. 56 | 57 | There are two good usecases for the feature: 58 | 59 | 1. Sort out testing and stable versions (as in example above) 60 | 61 | 2. Use a single ``bulk repo-add`` command to add packages for every 62 | distro. This works by append something like ``+bionic1`` suffix 63 | to a package version and add a respective ``match-version`` 64 | for that distribution. 65 | 66 | ``skip-version`` 67 | This is the same as ``match-version`` but is a negative filter. If 68 | both are matched ``skip-version`` takes precedence. 69 | 70 | ``add-empty-i386-repo`` 71 | (default ``false``) When building ``amd64``-only repo also add an empty 72 | index for ``i386`` counterpart. This is needed to prevent errors on 73 | ``apt update`` on systems which are configured to fetch both 64bit and 74 | 32bit versions of packages. 75 | 76 | For now it's known that ubuntu precise (12.04) default install only has 77 | this problem. So since precise reached its end of life this option is 78 | deprecated. 79 | -------------------------------------------------------------------------------- /src/config.rs: -------------------------------------------------------------------------------- 1 | use std::path::{Path, PathBuf}; 2 | 3 | use quire::validate::{Sequence, Structure, Enum, Nothing, Numeric, Scalar}; 4 | use quire::{parse_config, Options}; 5 | 6 | use version::Version; 7 | use bulk_version::MinimumVersion; 8 | 9 | 10 | #[derive(Deserialize, Clone, Debug)] 11 | pub struct Metadata { 12 | pub name: String, 13 | pub short_description: String, 14 | pub long_description: String, 15 | pub depends: Option, 16 | } 17 | 18 | #[allow(non_camel_case_types)] 19 | #[derive(Deserialize, Clone, Copy, Debug)] 20 | pub enum RepositoryType { 21 | debian, 22 | } 23 | 24 | #[derive(Deserialize, Clone, Debug)] 25 | pub struct Repository { 26 | pub kind: RepositoryType, 27 | pub suite: Option, 28 | pub component: Option, 29 | pub keep_releases: Option, 30 | pub match_version: Option, 31 | pub skip_version: Option, 32 | // This hack is needed for old ubuntu which want to download indexes for 33 | // i386 packages even on amd64 even if you will never try to install them 34 | pub add_empty_i386_repo: bool, 35 | } 36 | 37 | #[derive(Deserialize, Clone, Debug)] 38 | pub struct Config { 39 | pub minimum_bulk: Version, 40 | pub metadata: Option, 41 | pub repositories: Vec, 42 | pub versions: Vec, 43 | } 44 | 45 | #[derive(Deserialize, Clone, Debug)] 46 | pub struct VersionHolder { 47 | pub block_start: Option, 48 | pub block_end: Option, 49 | pub multiple_blocks: bool, 50 | pub file: Option, 51 | pub files: Vec, 52 | pub regex: String, 53 | pub partial_version: Option, 54 | } 55 | 56 | impl Config { 57 | fn validator<'x>() -> Structure<'x> { 58 | Structure::new() 59 | .member("minimum_bulk", MinimumVersion( 60 | Version(env!("CARGO_PKG_VERSION")))) 61 | .member("metadata", Structure::new().optional() 62 | .member("name", Scalar::new()) 63 | .member("short_description", Scalar::new()) 64 | .member("long_description", Scalar::new()) 65 | .member("depends", Scalar::new().optional())) 66 | .member("repositories", Sequence::new(Structure::new() 67 | .member("kind", Enum::new().allow_plain() 68 | .option("debian", Nothing) 69 | ) 70 | .member("suite", Scalar::new().optional()) 71 | .member("component", Scalar::new().optional()) 72 | .member("keep_releases", Numeric::new().optional()) 73 | .member("match_version", Scalar::new().optional()) 74 | .member("skip_version", Scalar::new().optional()) 75 | .member("add_empty_i386_repo", Scalar::new().default(false)))) 76 | .member("versions", Sequence::new(Structure::new() 77 | .member("block_start", Scalar::new().optional()) 78 | .member("block_end", Scalar::new().optional()) 79 | .member("multiple_blocks", Scalar::new().default(false)) 80 | .member("file", Scalar::new().optional()) 81 | .member("files", Sequence::new(Scalar::new())) 82 | .member("regex", Scalar::new()) 83 | .member("partial_version", Scalar::new().optional()))) 84 | } 85 | pub fn parse_file(p: &Path) -> Result { 86 | Ok(parse_config(p, &Config::validator(), &Options::default()) 87 | .map_err(|e| e.to_string())?) 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | ==== 2 | Bulk 3 | ==== 4 | 5 | Bulk is a super-simple packaging utility. It's similar to fpm_ but implemented 6 | in rust. 7 | 8 | It does three things for you: 9 | 10 | * Makes directory of files into deb package 11 | * Maintains a number of repos (stable, testing...) from list of packages 12 | * Updates your version numbers 13 | 14 | .. _fpm: https://github.com/jordansissel/fpm 15 | 16 | :Status: Alpha 17 | 18 | 19 | Why? 20 | ==== 21 | 22 | Default packaging tools for debian are too complex. Also I wanted: 23 | 24 | 1. Simple to install zero-dependency tool (comparing to fpm_) 25 | 2. Experiment a little bit with reproducible packages (i.e. omit timestamps 26 | from a package) 27 | 3. Simple utility to maintain (multiple) repositories 28 | 4. Add tiny wrapper around vagga to actually build the packages for all 29 | distributions by single command 30 | 31 | It turned out that all functionality I needed from fpm_ could be reimplemented 32 | in a night, so we have a new tool, ready for the new experiments. 33 | 34 | 35 | Limitations 36 | =========== 37 | 38 | Bulk should be simple. While we may lift few limitation in future versions we 39 | don't aim to support all the features. 40 | 41 | Limitations are: 42 | 43 | 1. No install scripts 44 | 2. All files owned by root and no timestamps 45 | 3. No devices, sockets, empty dirs and other possible habitants of 46 | tar/deb archive 47 | 4. Limited support of package metadata (focusing on common between different 48 | linux distributions) 49 | 50 | 51 | Installation 52 | ============ 53 | 54 | Currently we provide static binary for x86_64: 55 | 56 | wget http://files.zerogw.com/bulk/bulk-0.4.12.tar.gz 57 | tar -xzf bulk-0.4.12.tar.gz -C / 58 | 59 | Or you can install it with cargo: 60 | 61 | cargo install bulk 62 | 63 | This will install bulk in ``/usr/bin``. Ubuntu packages will be available 64 | shortly. 65 | 66 | 67 | How To Use 68 | ========== 69 | 70 | Build program and install to some directory, say ``pkg``. Put some metadata 71 | into ``bulk.yaml``. Then pack it into a debian package:: 72 | 73 | bulk pack --config bulk.yaml --dir pkg --dest-dir dist 74 | 75 | And you will get a package in ``dist`` directory. You may find the example 76 | ``bulk.yaml`` in this repository. 77 | 78 | 79 | Building Packages 80 | ================= 81 | 82 | Just a few examples on how to prepare things to be packaged. With autotools 83 | it looks like this:: 84 | 85 | ./configure --prefix=/usr 86 | make 87 | rm -rf pkg 88 | make install DESTDIR=$(pwd)/pkg 89 | bulk pack --config bulk.yaml --dir pkg --dest-dir dist 90 | 91 | Or with new ``cargo install``:: 92 | 93 | rm -rf pkg 94 | cargo install PACKAGE_NAME --root ./pkg/usr 95 | rm pkg/usr/.crates.toml 96 | bulk pack --config bulk.yaml --dir pkg --dest-dir dist 97 | 98 | This way you may package crate from crates.io. 99 | 100 | 101 | ======= 102 | License 103 | ======= 104 | 105 | Licensed under either of 106 | 107 | * Apache License, Version 2.0, (./LICENSE-APACHE or http://www.apache.org/licenses/LICENSE-2.0) 108 | * MIT license (./LICENSE-MIT or http://opensource.org/licenses/MIT) 109 | 110 | at your option. 111 | 112 | ------------ 113 | Contribution 114 | ------------ 115 | 116 | Unless you explicitly state otherwise, any contribution intentionally 117 | submitted for inclusion in the work by you, as defined in the Apache-2.0 118 | license, shall be dual licensed as above, without any additional terms or 119 | conditions. 120 | -------------------------------------------------------------------------------- /doc/config/versions.rst: -------------------------------------------------------------------------------- 1 | Versions 2 | ======== 3 | 4 | Versions section help you with bookkeeping a version in your application, 5 | here is the sample of versioning for python application 6 | 7 | .. code-block:: yaml 8 | 9 | versions: 10 | - file: setup.py 11 | regex: ^\s*version\s*=\s*["']([^"']+)["'] 12 | - file: your_module/__init__.py 13 | regex: ^__version__\s*=\s*["']([^"']+)["'] 14 | 15 | You might also add an example to your readme and keep version in documentation 16 | updated too: 17 | 18 | .. code-block:: yaml 19 | 20 | versions: 21 | 22 | - file: setup.py 23 | regex: ^\s*version\s*=\s*["']([^"']+)["'] 24 | 25 | - file: your_module/__init__.py 26 | regex: ^__version__\s*=\s*["']([^"']+)["'] 27 | 28 | - file: doc/conf.py 29 | regex: ^version\s*=\s*u?["']([^"']+)["'] 30 | partial-version: ^\d+\.\d+ # no patch version 31 | 32 | - file: doc/conf.py 33 | regex: ^release\s*=\s*u?["']([^"']+)["'] 34 | 35 | - file: README.rst 36 | regex: pip\s+install\s+your-module==(\S+) 37 | 38 | 39 | Options: 40 | 41 | ``file`` 42 | Filename to search version in, relative to project directory (usually 43 | a directory that contains ``bulk.yaml``) 44 | 45 | ``files`` 46 | A list of files to search. This is useful if you can use same regex in 47 | multiple files. 48 | 49 | .. note:: Neither existence of ``file`` or any one in ``files`` 50 | is enforced. if you make a typo file will be silently skipped. 51 | Always use ``bulk check-version`` after modifying rules. 52 | 53 | On the upside is that you can use same ``bulk.yaml`` for many similar 54 | projects and versions that aren't present will be skipped. 55 | 56 | 57 | ``regex`` 58 | A regular expression that matches version. It must contain a single 59 | capturing group (i.e. ``a (parenthised expression)``) for capturing 60 | actual version. Regex can match only on a single line. 61 | 62 | The expression shouldn't be too strict and should not try to validate 63 | the version number itself. I.e. if version is quoted anything inside the 64 | quotes should be considered version, if it isn't anything to next white 65 | space or newline is okay. 66 | 67 | Too strict version pattern risk to be either to replace 68 | ``1.2.3`` to ``1.2.4`` in ``1.2.3-beta.1`` keeping beta suffix, or to 69 | skip ``1.2.3-beta.1`` line in a file without updating it because it 70 | doesn't match. 71 | 72 | If ``regex`` matches multiple times all matching lines are treated as 73 | version number. Also multiple entries with the same file and different 74 | rules can be configured. 75 | 76 | ``partial-version`` 77 | A regular expression that allows to select only portion of version 78 | number. Few examples: 79 | 80 | 1. ``^\d+\.\d+`` -- selects major.minor version but not patch 81 | 2. ``-.*$`` -- selects ``-alpha``, ``-beta.1``, ``-31-g12bd530`` or any 82 | other pre-release suffix in version number. 83 | 84 | ``block-start``, ``block-end`` 85 | Marks block where to find version number in. 86 | 87 | For example, in ``Cargo.toml`` version number is in the ``[package]`` 88 | section and named ``version``, whereas ``version=`` in other case may 89 | denote version of other things like pedependencies. So we use this: 90 | 91 | .. code-block:: yaml 92 | 93 | - file: Cargo.toml 94 | block-start: ^\[package\] 95 | block-end: ^\[.*\] 96 | regex: ^version\s*=\s*"(\S+)" 97 | 98 | You can specify single file multiple times in versions section. Which 99 | effectively means you can fix version in multiple different sections. 100 | 101 | ``multiple-blocks`` 102 | (default ``false``) By default bulk stops scanning this file for this rule 103 | on the first ``block-end`` after ``block-start``. If this setting is set 104 | to ``true`` searches for the next ``block-start`` istead. This option 105 | does nothing if no block defined. 106 | -------------------------------------------------------------------------------- /src/repo/mod.rs: -------------------------------------------------------------------------------- 1 | mod metadata; 2 | mod ar; 3 | mod deb; 4 | mod debian; 5 | 6 | use std::io::{stdout, stderr, Write}; 7 | use std::path::{Path, PathBuf}; 8 | use std::process::exit; 9 | 10 | use failure::{Error, err_msg}; 11 | use regex::Regex; 12 | use argparse::{ArgumentParser, Parse, Collect, StoreConst}; 13 | 14 | use config::{Config, RepositoryType}; 15 | use repo::metadata::gather_metadata; 16 | 17 | 18 | fn _repo_add(config: &Path, packages: &Vec, dir: &Path, 19 | on_conflict: debian::ConflictResolution) 20 | -> Result<(), Error> 21 | { 22 | let packages = packages.iter().map(gather_metadata) 23 | .collect::, _>>()?; 24 | debug!("Packages read {:#?}", packages); 25 | let cfg = Config::parse_file(&config) 26 | .map_err(|e| format_err!("can't parse config {:?}: {}", config, e))?; 27 | let mut debian = debian::Repository::new(dir); 28 | 29 | for repo in &cfg.repositories { 30 | let version_re = match repo.match_version { 31 | Some(ref re) => Some(Regex::new(re)?), 32 | None => None, 33 | }; 34 | let skip_re = match repo.skip_version { 35 | Some(ref re) => Some(Regex::new(re)?), 36 | None => None, 37 | }; 38 | let matching = packages.iter() 39 | .filter(|p| { 40 | version_re.as_ref().map(|x| x.is_match(&p.version)) 41 | .unwrap_or(true) && 42 | !skip_re.as_ref().map(|x| x.is_match(&p.version)) 43 | .unwrap_or(false) 44 | }) 45 | .collect::>(); 46 | if matching.len() > 0 { 47 | match (repo.kind, &repo.suite, &repo.component) { 48 | (RepositoryType::debian, &Some(ref suite), &Some(ref comp)) 49 | => { 50 | for p in matching { 51 | debian.open(suite, comp, &p.arch)? 52 | .add_package(p, on_conflict)?; 53 | if repo.add_empty_i386_repo && p.arch != "i386" { 54 | debian.open(suite, comp, "i386")?; 55 | } 56 | } 57 | } 58 | (RepositoryType::debian, _, _) => { 59 | return Err(err_msg("Debian repository requires suite and \ 60 | component to be specified")); 61 | 62 | } 63 | } 64 | } 65 | } 66 | for repo in cfg.repositories { 67 | match (repo.kind, &repo.suite, &repo.component) { 68 | (RepositoryType::debian, &Some(ref suite), &Some(ref comp)) => { 69 | if let Some(limit) = repo.keep_releases { 70 | debian.trim(suite, comp, limit); 71 | } 72 | } 73 | _ => unreachable!(), 74 | } 75 | } 76 | debian.write()?; 77 | // TODO(tailhook) remove removed files 78 | Ok(()) 79 | } 80 | 81 | 82 | pub fn repo_add(args: Vec) { 83 | let mut config = PathBuf::from("bulk.yaml"); 84 | let mut repo_dir = PathBuf::new(); 85 | let mut packages = Vec::::new(); 86 | let mut conflict = debian::ConflictResolution::Error; 87 | { 88 | let mut ap = ArgumentParser::new(); 89 | ap.refer(&mut config) 90 | .add_option(&["-c", "--config"], Parse, 91 | "Package configuration file"); 92 | ap.refer(&mut repo_dir) 93 | .add_option(&["-D", "--repository-base"], Parse, 94 | "Directory where repositories are stored"); 95 | ap.refer(&mut conflict) 96 | .add_option(&["--skip-existing"], 97 | StoreConst(debian::ConflictResolution::Keep), 98 | "Skip package if it's already in the repository") 99 | .add_option(&["--replace-existing"], 100 | StoreConst(debian::ConflictResolution::Replace), 101 | "Replace package if it's already in the repository"); 102 | ap.refer(&mut packages) 103 | .add_argument("packages", Collect, 104 | "Package file names to add"); 105 | match ap.parse(args, &mut stdout(), &mut stderr()) { 106 | Ok(()) => {} 107 | Err(x) => exit(x), 108 | } 109 | } 110 | 111 | match _repo_add(&config, &packages, &repo_dir, conflict) { 112 | Ok(()) => {} 113 | Err(err) => { 114 | writeln!(&mut stderr(), "Error: {}", err).ok(); 115 | exit(1); 116 | } 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /src/ver/scanner.rs: -------------------------------------------------------------------------------- 1 | use std::io::{self, BufRead}; 2 | use std::sync::Arc; 3 | 4 | use re; 5 | use config::VersionHolder; 6 | 7 | 8 | #[derive(Clone)] 9 | pub struct Scanner { 10 | pub block_start: Option>, 11 | pub block_end: Option>, 12 | pub multiple_blocks: bool, 13 | pub regex: Arc, 14 | pub partial: Option>, 15 | } 16 | 17 | pub struct Lines(usize, B); 18 | 19 | impl Iterator for Lines { 20 | type Item = io::Result<(usize, String)>; 21 | fn next(&mut self) -> Option> { 22 | let Lines(ref mut lineno, ref mut file) = *self; 23 | let mut s = String::with_capacity(200); 24 | match file.read_line(&mut s) { 25 | Ok(0) => None, 26 | Ok(_) => { 27 | *lineno += 1; 28 | Some(Ok((*lineno, s))) 29 | } 30 | Err(e) => Some(Err(e)), 31 | } 32 | } 33 | } 34 | 35 | impl Lines { 36 | pub fn iter(file: B) -> Lines { 37 | Lines(0, file) 38 | } 39 | } 40 | 41 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] 42 | enum State { 43 | Prefix, 44 | Body, 45 | Suffix, 46 | } 47 | 48 | pub struct Iter<'a> { 49 | scanner: &'a Scanner, 50 | state: State, 51 | error: Option, 52 | } 53 | 54 | quick_error! { 55 | #[derive(Debug)] 56 | pub enum Error { 57 | EmptyVersion(line_no: usize) { 58 | display("{}: empty version string captured", line_no) 59 | description("empty version") 60 | } 61 | NoCapture(line_no: usize) { 62 | display("{}: no capture in regex, probably bad regex", line_no) 63 | description("no capture in regex, probably bad regex") 64 | } 65 | } 66 | } 67 | 68 | impl<'a> Iter<'a> { 69 | 70 | pub fn error(self) -> Result<(), Error> { 71 | self.error.map(Err).unwrap_or(Ok(())) 72 | } 73 | 74 | pub fn scanner(&self) -> &Scanner { 75 | self.scanner 76 | } 77 | 78 | pub fn line(&mut self, line_no: usize, line: &str) 79 | -> Option<(usize, usize)> 80 | { 81 | use self::State::*; 82 | use self::Error::*; 83 | if self.error.is_some() { 84 | return None; 85 | } 86 | match self.state { 87 | Prefix => { 88 | if self.scanner.block_start.as_ref().unwrap().is_match(line) { 89 | self.state = Body; 90 | } 91 | None 92 | } 93 | Body => match self.scanner.regex.captures(line) { 94 | Some(cpt) => match cpt.get(1) { 95 | Some(m) if m.start() == m.end() => { 96 | self.error = Some(EmptyVersion(line_no)); 97 | return None; 98 | } 99 | Some(m) => Some((m.start(), m.end())), 100 | None => { 101 | self.error = Some(NoCapture(line_no)); 102 | return None; 103 | } 104 | }, 105 | None => { 106 | match self.scanner.block_end { 107 | Some(ref end_re) if end_re.is_match(line) => { 108 | if self.scanner.multiple_blocks { 109 | self.state = Prefix; 110 | } else { 111 | self.state = Suffix; 112 | } 113 | } 114 | _ => {} 115 | } 116 | None 117 | } 118 | }, 119 | Suffix => None, 120 | } 121 | } 122 | } 123 | 124 | impl Scanner { 125 | pub fn new(cfg: &VersionHolder) -> Result { 126 | Ok(Scanner { 127 | block_start: if let Some(ref regex) = cfg.block_start { 128 | Some(try!(re::compile(regex))) 129 | } else { None }, 130 | block_end: if let Some(ref regex) = cfg.block_end { 131 | Some(try!(re::compile(regex))) 132 | } else { None }, 133 | regex: try!(re::compile(&cfg.regex)), 134 | multiple_blocks: cfg.multiple_blocks, 135 | partial: if let Some(ref regex) = cfg.partial_version { 136 | Some(try!(re::compile(regex))) 137 | } else { None }, 138 | }) 139 | } 140 | pub fn start(&self) -> Iter { 141 | use self::State::*; 142 | Iter { 143 | scanner: self, 144 | state: if self.block_start.is_some() 145 | { Prefix } else { Body }, 146 | error: None, 147 | } 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /src/pack/mod.rs: -------------------------------------------------------------------------------- 1 | mod ar; 2 | mod tar; 3 | mod deb; 4 | 5 | use std::io; 6 | use std::io::{stdout, stderr, Write, BufWriter}; 7 | use std::env; 8 | use std::fs::{File, create_dir, rename, remove_file}; 9 | use std::path::{Path, PathBuf}; 10 | use std::error::Error; 11 | use std::process::exit; 12 | 13 | use argparse::{ArgumentParser, Parse, ParseOption}; 14 | use tar::{Builder as Archive}; 15 | use libflate::gzip; 16 | use scan_dir; 17 | 18 | use ver; 19 | use version::Version; 20 | use config::{Config, Metadata}; 21 | use self::ar::{ArArchive, SIZE_AUTO}; 22 | use self::tar::ArchiveExt; 23 | use self::deb::format_deb_control; 24 | 25 | 26 | fn write_deb(dest: &Path, dir: &Path, meta: &Metadata, version: &String) 27 | -> Result<(), io::Error> 28 | { 29 | let mtime = env::var("SOURCE_DATE_EPOCH").ok() 30 | .and_then(|x| x.parse().ok()).unwrap_or(1); 31 | let file = BufWriter::new(File::create(&dest)?); 32 | let mut ar = try!(ArArchive::new(file)); 33 | 34 | try!(ar.add("debian-binary", mtime, 0, 0, 0o100644, 4) 35 | .and_then(|mut f| f.write_all(b"2.0\n"))); 36 | 37 | { 38 | let control = try!(ar.add("control.tar.gz", 39 | mtime, 0, 0, 0o100644, SIZE_AUTO)); 40 | let mut creal = gzip::Encoder::new(control)?; 41 | { 42 | let mut arch = Archive::new(&mut creal); 43 | let mut buf = Vec::with_capacity(1024); 44 | format_deb_control(&mut buf, &meta, version, "amd64")?; 45 | arch.append_blob("control", mtime, &buf)?; 46 | arch.finish()?; 47 | } 48 | creal.finish().into_result()?; 49 | } 50 | { 51 | let data = try!(ar.add("data.tar.gz", 52 | mtime, 0, 0, 0o100644, SIZE_AUTO)); 53 | let mut dreal = gzip::Encoder::new(data)?; 54 | let mut files = try!(scan_dir::ScanDir::all().skip_backup(true) 55 | .walk(dir, |iter| { 56 | iter.map(|(entry, _name)| { 57 | entry.path().strip_prefix(dir).unwrap().to_path_buf()}) 58 | .collect::>() 59 | }).map_err(|errs| io::Error::new(io::ErrorKind::InvalidData, 60 | errs.iter().map(ToString::to_string).collect::>()[..] 61 | .join("\n")))); 62 | files.sort(); 63 | { 64 | let mut arch = Archive::new(&mut dreal); 65 | for fpath in files { 66 | arch.append_file_at(dir, fpath, mtime)?; 67 | } 68 | arch.finish()?; 69 | } 70 | dreal.finish().into_result()?; 71 | } 72 | Ok(()) 73 | } 74 | 75 | fn _pack(config: &Path, dir: &Path, destdir: &Path, 76 | version: Option>) 77 | -> Result<(), Box> 78 | { 79 | let cfg = try!(Config::parse_file(config)); 80 | 81 | let version = if let Some(ver) = version { 82 | ver.num().to_string() 83 | } else { 84 | try!(ver::get(&cfg, Path::new("."))).0 85 | }; 86 | 87 | let ref meta = try!(cfg.metadata 88 | .ok_or(format!("No package metadata is in the config"))); 89 | // TODO(tailhook) not only debian 90 | let dest = destdir.join(format!("{}-{}_{}.deb", 91 | meta.name, version, "amd64")); 92 | if !destdir.exists() { 93 | try!(create_dir(&destdir) 94 | .map_err(|e| format!("Can't create destination dir: {}", e))); 95 | } 96 | 97 | let tmpname = dest.with_extension(".deb.tmp"); 98 | try!(write_deb(&tmpname, dir, &meta, &version) 99 | .map_err(|e| format!("Error writing deb: {}", e))); 100 | if dest.exists() { 101 | try!(remove_file(&dest) 102 | .map_err(|e| format!("Can't remove old package: {}", e))); 103 | } 104 | try!(rename(&tmpname, &dest) 105 | .map_err(|e| format!("Can't rename deb to target place: {}", e))); 106 | println!("Written {}", dest.display()); 107 | Ok(()) 108 | } 109 | 110 | 111 | pub fn pack(args: Vec) { 112 | let mut config = PathBuf::from("bulk.yaml"); 113 | let mut dir = PathBuf::from("pkg"); 114 | let mut destdir = PathBuf::from("dist"); 115 | let mut version = None; 116 | { 117 | let mut ap = ArgumentParser::new(); 118 | ap.refer(&mut config) 119 | .add_option(&["-c", "--config"], Parse, 120 | "Package configuration file"); 121 | ap.refer(&mut dir) 122 | .add_option(&["-d", "--dir"], Parse, 123 | "Directory that will be a root of filesystem in a package"); 124 | ap.refer(&mut destdir) 125 | .add_option(&["-D", "--dest-dir"], Parse, 126 | "Directory to put package to"); 127 | ap.refer(&mut version) 128 | .add_option(&["--package-version"], ParseOption, 129 | "Force package version instead of discovering it."); 130 | match ap.parse(args, &mut stdout(), &mut stderr()) { 131 | Ok(()) => {} 132 | Err(x) => exit(x), 133 | } 134 | } 135 | 136 | match _pack(&config, &dir, &destdir, version) { 137 | Ok(()) => {} 138 | Err(text) => { 139 | writeln!(&mut stderr(), "Error: {}", text).ok(); 140 | exit(1); 141 | } 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /vagga.yaml: -------------------------------------------------------------------------------- 1 | commands: 2 | 3 | make: !Command 4 | description: Build executable 5 | container: rust-musl 6 | run: [cargo, build] 7 | 8 | make-static: !Command 9 | description: Build executable 10 | container: rust-musl 11 | run: [cargo, build, --target=x86_64-unknown-linux-musl] 12 | 13 | test: !Command 14 | description: Run unit tests 15 | container: rust-musl 16 | run: [cargo, test] 17 | 18 | cargo: !Command 19 | description: Run any cargo command 20 | container: rust-musl 21 | run: [cargo] 22 | 23 | bulk: !Command 24 | description: Run debugging version of bulk 25 | container: rust-musl 26 | prerequisites: [make] 27 | run: [/work/target/debug/bulk] 28 | 29 | package: !Command 30 | description: Package itself 31 | container: rust-musl 32 | prerequisites: [make] 33 | accepts-arguments: true 34 | run: | 35 | rm -rf pkg 36 | mkdir pkg 37 | version="$(git describe --dirty)" 38 | ./target/debug/bulk with-version "$version" \ 39 | cargo build --target=x86_64-unknown-linux-musl --release 40 | install -D target/x86_64-unknown-linux-musl/release/bulk pkg/usr/bin/bulk 41 | # pack by itself 42 | ./target/debug/bulk with-version "$version" \ 43 | /work/pkg/usr/bin/bulk pack --dir ./pkg "$@" 44 | 45 | repo-add: !Command 46 | description: "Add `*.deb` to test repository in dist/repos" 47 | container: rust-musl 48 | prerequisites: [make] 49 | accepts-arguments: true 50 | run: | 51 | target/debug/bulk repo-add \ 52 | --config ./bulk.yaml --repository-base ./dist/repos \ 53 | --replace-existing \ 54 | dist/*.deb "$@" 55 | 56 | get-version: !Command 57 | description: "Get version of this package (mostly to test bulk)" 58 | container: rust-musl 59 | prerequisites: [make] 60 | run: [target/debug/bulk, get-version] 61 | 62 | check-version: !Command 63 | description: "Check version of this package (mostly to test bulk)" 64 | container: rust-musl 65 | prerequisites: [make] 66 | run: [target/debug/bulk, check-version] 67 | 68 | set-version: !Command 69 | description: "Set version of this package (mostly to test bulk)" 70 | container: rust-musl 71 | prerequisites: [make] 72 | run: [target/debug/bulk, set-version] 73 | 74 | with-version: !Command 75 | description: "Run command with version set (mostly to test bulk)" 76 | container: rust-musl 77 | prerequisites: [make] 78 | run: [target/debug/bulk, with-version] 79 | 80 | _test-trusty: !Command 81 | container: trusty-install 82 | write-mode: transient-hard-link-copy 83 | run: &installtest | 84 | set -ex 85 | /usr/bin/bulk --help && exit 1 86 | webfsd -r /work/dist/repos -p 7777 & 87 | apt-get update 88 | apt-get install -y --allow-unauthenticated bulk 89 | /usr/bin/bulk --version 90 | 91 | _test-xenial: !Command 92 | container: xenial-install 93 | write-mode: transient-hard-link-copy 94 | run: *installtest 95 | 96 | _test-bionic: !Command 97 | container: bionic-install 98 | write-mode: transient-hard-link-copy 99 | run: *installtest 100 | 101 | full-cycle-test: !Command 102 | description: "Build package, add to repo and install" 103 | container: rust-musl 104 | prerequisites: [package, repo-add, _test-trusty, _test-xenial, _test-bionic] 105 | run: [echo, "Done"] 106 | 107 | doc: !Command 108 | description: Build docs 109 | container: doc 110 | work-dir: doc 111 | run: [make, html] 112 | 113 | containers: 114 | 115 | rust-musl: 116 | environ: &rustenv 117 | LD_LIBRARY_PATH: /musl/lib/rustlib/x86_64-unknown-linux-musl/lib 118 | PATH: /musl/bin:/usr/local/bin:/usr/bin:/bin 119 | HOME: /work/target 120 | setup: 121 | - !Ubuntu bionic 122 | - !UbuntuUniverse 123 | - !Install [build-essential, ca-certificates, musl-tools, cmake, zlib1g-dev, vim] 124 | - !TarInstall 125 | url: "https://static.rust-lang.org/dist/rust-1.27.0-x86_64-unknown-linux-gnu.tar.gz" 126 | script: "./install.sh --prefix=/usr --components=rustc,rust-std-x86_64-unknown-linux-gnu,cargo" 127 | - !TarInstall 128 | url: "https://static.rust-lang.org/dist/rust-std-1.27.0-x86_64-unknown-linux-musl.tar.gz" 129 | script: "./install.sh --prefix=/musl \ 130 | --components=rust-std-x86_64-unknown-linux-musl" 131 | - !Sh 'ln -s /musl/lib/rustlib/x86_64-unknown-linux-musl /usr/lib/rustlib/x86_64-unknown-linux-musl' 132 | 133 | # For packaging 134 | - !Install [git] 135 | 136 | trusty-install: 137 | setup: 138 | - !Ubuntu trusty 139 | - !UbuntuUniverse 140 | - !Install [webfs] 141 | - !UbuntuRepo 142 | url: http://localhost:7777/ 143 | suite: static 144 | components: [bulk-testing] 145 | trusted: true 146 | 147 | xenial-install: 148 | setup: 149 | - !Ubuntu xenial 150 | - !UbuntuUniverse 151 | - !Install [webfs] 152 | - !UbuntuRepo 153 | url: http://localhost:7777/ 154 | suite: static 155 | components: [bulk-testing] 156 | trusted: true 157 | 158 | bionic-install: 159 | setup: 160 | - !Ubuntu bionic 161 | - !UbuntuUniverse 162 | - !Install [webfs] 163 | - !UbuntuRepo 164 | url: http://localhost:7777/ 165 | suite: static 166 | components: [bulk-testing] 167 | trusted: true 168 | 169 | doc: 170 | setup: 171 | - !Alpine v3.7 172 | - !Install [make, python3] 173 | - !PipConfig { dependencies: true } 174 | - !Py3Install [sphinx] 175 | - !Py3Requirements doc/requirements.txt 176 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | extern crate argparse; 2 | extern crate env_logger; 3 | extern crate libflate; 4 | extern crate git2; 5 | extern crate quire; 6 | extern crate regex; 7 | extern crate serde; 8 | extern crate scan_dir; 9 | extern crate sha2; 10 | extern crate tar; 11 | extern crate tempfile; 12 | extern crate time; 13 | extern crate unicase; 14 | #[macro_use] extern crate failure; 15 | #[macro_use] extern crate log; 16 | #[macro_use] extern crate matches; 17 | #[macro_use] extern crate lazy_static; 18 | #[macro_use] extern crate quick_error; 19 | #[macro_use] extern crate serde_derive; 20 | 21 | 22 | mod config; 23 | mod deb_ext; 24 | mod hash_file; 25 | mod version; 26 | mod bulk_version; 27 | mod re; 28 | 29 | mod repo; 30 | mod pack; 31 | mod ver; 32 | 33 | use std::str::FromStr; 34 | 35 | use argparse::{ArgumentParser, Store, Print, List}; 36 | 37 | 38 | enum Action { 39 | Help, 40 | Pack, 41 | RepoAdd, 42 | GetVersion, 43 | SetVersion, 44 | IncrVersion, 45 | CheckVersion, 46 | WithVersion, 47 | WithGitVersion, 48 | } 49 | 50 | impl FromStr for Action { 51 | type Err = (); 52 | fn from_str(value: &str) -> Result { 53 | match value { 54 | "help" => Ok(Action::Help), 55 | 56 | "pack" => Ok(Action::Pack), 57 | 58 | "repo-add" => Ok(Action::RepoAdd), 59 | "repo_add" => Ok(Action::RepoAdd), 60 | "repoadd" => Ok(Action::RepoAdd), 61 | "radd" => Ok(Action::RepoAdd), 62 | "add-to-repo" => Ok(Action::RepoAdd), 63 | "add_to_repo" => Ok(Action::RepoAdd), 64 | "addtorepo" => Ok(Action::RepoAdd), 65 | 66 | "getversion" => Ok(Action::GetVersion), 67 | "get-version" => Ok(Action::GetVersion), 68 | "getver" => Ok(Action::GetVersion), 69 | "get-ver" => Ok(Action::GetVersion), 70 | "ver-get" => Ok(Action::GetVersion), 71 | "version-get" => Ok(Action::GetVersion), 72 | "verget" => Ok(Action::GetVersion), 73 | "versionget" => Ok(Action::GetVersion), 74 | 75 | "set-version" => Ok(Action::SetVersion), 76 | "set-ver" => Ok(Action::SetVersion), 77 | "setversion" => Ok(Action::SetVersion), 78 | "setver" => Ok(Action::SetVersion), 79 | "ver-set" => Ok(Action::SetVersion), 80 | "version-set" => Ok(Action::SetVersion), 81 | "verset" => Ok(Action::SetVersion), 82 | "versionset" => Ok(Action::SetVersion), 83 | 84 | "incr-version" => Ok(Action::IncrVersion), 85 | "inc-version" => Ok(Action::IncrVersion), 86 | "version-incr" => Ok(Action::IncrVersion), 87 | "version-inc" => Ok(Action::IncrVersion), 88 | "incr" => Ok(Action::IncrVersion), 89 | "bump-version" => Ok(Action::IncrVersion), 90 | "version-bump" => Ok(Action::IncrVersion), 91 | "bumpver" => Ok(Action::IncrVersion), 92 | "bump-ver" => Ok(Action::IncrVersion), 93 | "bump" => Ok(Action::IncrVersion), 94 | 95 | "check-version" => Ok(Action::CheckVersion), 96 | "version-check" => Ok(Action::CheckVersion), 97 | 98 | "with-version" => Ok(Action::WithVersion), 99 | "with-git-version" => Ok(Action::WithGitVersion), 100 | 101 | _ => Err(()) 102 | } 103 | } 104 | } 105 | 106 | 107 | fn main() { 108 | let mut command = Action::Help; 109 | let mut args = Vec::::new(); 110 | { 111 | let mut ap = ArgumentParser::new(); 112 | ap.add_option(&["-V", "--version"], 113 | Print(env!("CARGO_PKG_VERSION").to_string()), 114 | "Show version of bulk and exit"); 115 | ap.refer(&mut command) 116 | .add_argument("command", Store, " 117 | Command to run. Supported commands: \ 118 | pack, repo-add, get-version, set-version, incr-version, \ 119 | check-version, with-version, with-git-version"); 120 | ap.refer(&mut args) 121 | .add_argument("arguments", List, 122 | "Arguments for the command"); 123 | ap.stop_on_first_argument(true); 124 | ap.parse_args_or_exit(); 125 | } 126 | env_logger::init(); 127 | match command { 128 | Action::Help => { 129 | println!("Usage:"); 130 | println!(" bulk \ 131 | {{pack,repo-add,get-version,set-version,\ 132 | check-version,with-version,with-git-version}} \ 133 | [options]"); 134 | } 135 | Action::Pack => { 136 | args.insert(0, "bulk pack".to_string()); 137 | pack::pack(args); 138 | } 139 | Action::RepoAdd => { 140 | args.insert(0, "bulk repo-add".to_string()); 141 | repo::repo_add(args); 142 | } 143 | Action::GetVersion => { 144 | args.insert(0, "bulk get-version".to_string()); 145 | ver::get_version(args); 146 | } 147 | Action::SetVersion => { 148 | args.insert(0, "bulk set-version".to_string()); 149 | ver::set_version(args); 150 | } 151 | Action::IncrVersion => { 152 | args.insert(0, "bulk incr-version".to_string()); 153 | ver::incr_version(args); 154 | } 155 | Action::CheckVersion => { 156 | args.insert(0, "bulk check-version".to_string()); 157 | ver::check_version(args); 158 | } 159 | Action::WithVersion => { 160 | args.insert(0, "bulk with-version".to_string()); 161 | ver::with_version(args); 162 | } 163 | Action::WithGitVersion => { 164 | args.insert(0, "bulk with-git-version".to_string()); 165 | ver::with_git_version(args); 166 | } 167 | } 168 | } 169 | -------------------------------------------------------------------------------- /doc/version.rst: -------------------------------------------------------------------------------- 1 | Version Bookkeeping 2 | =================== 3 | 4 | Bulk can be used to sync version of your application to various places in code. 5 | 6 | 7 | Basics 8 | ------ 9 | 10 | Bulk uses regular expressions to find versions in some file. For example, 11 | here is how we track versions in typical python project: 12 | 13 | .. code-block:: yaml 14 | 15 | versions: 16 | 17 | # There is usually a version in setup.py 18 | - file: setup.py 19 | # this isn't 100% correct as version can end in different quote or 20 | # there might be few version parameters in a file, but this is good 21 | # enough for many projects, other projects might need to tweak matcher 22 | regex: ^\s*version\s*=\s*["']([^"']+)["'] 23 | 24 | 25 | # Also it's a good idea to put library version into 26 | # a __version__ attribute of the module itself 27 | - file: your_module/__init__.py 28 | regex: ^__version__\s*=\s*["']([^"']+)["'] 29 | 30 | Put it in ``bulk.yaml`` and now you can find out version with: 31 | 32 | .. code-block:: console 33 | 34 | > bulk get-version 35 | 1.3.5 36 | 37 | Yes, the first time you've written ``setup.py`` and ``__init__.py`` you needed 38 | to put version yourself. This is usually handled by project boilerplate. 39 | 40 | 41 | Releasing a Project 42 | ------------------- 43 | 44 | If you obey **semantic versioning** in the project version run one of: 45 | 46 | .. code-block:: console 47 | 48 | > bulk bump --breaking -g 49 | > bulk bump --feature -g 50 | > bulk bump --bugfix -g 51 | 52 | The commands above will increment a major, minor or patch version of your 53 | version number, commit the changes with a comment of 54 | ``Version bumped to v1.3.6`` and create an annotated tag ``v0.3.6`` by 55 | starting an editor and showing you changes since previous tag. You can opt-out 56 | commit and tag creation by omitting ``-g`` which is equivalent of longer 57 | ``--git-commit-and-tag``. 58 | 59 | You can also use ``-1``, ``-2`` and ``-3`` which increment the specific 60 | component of version. Technically they are are equivalent to above except 61 | when version is zero-based ``0.x``. 62 | 63 | .. note:: in case of ``0.x`` versions the version numbers are shifted. 64 | I.e. if you have two zeros numbers ``0.0.x`` any bump with increment a 65 | single version. If you have ``0.x.y`` number second component will increment 66 | with both ``--breaking`` and ``--feature``. This is how many existing tools 67 | handle semver. Use ``-1``, ``-2`` if in doubt or to switch from ``0.x`` 68 | versions to ``1.x``. 69 | 70 | For **date-based versioning** use: 71 | 72 | .. code-block:: console 73 | 74 | > bulk bump -dg 75 | 76 | This will force your version to something like ``v180317.0``. If you will 77 | subsequently run this command on the same day you will get ``v180317.1`` and 78 | so forth. 79 | 80 | .. note:: The date here is UTC to avoid issues with different people releasing 81 | in different timezones. 82 | 83 | Another way to update is to use ``set-version``: 84 | 85 | .. code-block:: console 86 | 87 | > bulk set-version v1.3.5-beta.1 88 | ./your_module/__init__.py:1: (v1.3.5 -> v1.3.5-beta.1) __version__ = '1.3.5-beta.1' 89 | ./setup.py:6: (v1.3.5 -> v1.3.5-beta.1) version='1.3.5-beta.1', 90 | 91 | This is useful to set some pre-release version as you see in example because we 92 | don't have a command-line flag for that or in case you have different version 93 | format or just want to skip version number for some reason. 94 | 95 | 96 | Building a Pre-Release Project 97 | ------------------------------ 98 | 99 | Everyting above assumes that version is stored in source code and commited to 100 | git. Which is true for many tools. But you don't want to commit version for 101 | a prerelease version of application. We have a nice command for this use 102 | case too: 103 | 104 | .. code-block:: console 105 | 106 | > bulk with-version v1.3.6-pre4 your-build-command 107 | 1.3.5 -> 1.3.6-pre4 108 | [ .. output of your-build-command .. ] 109 | 1.3.6-pre4 -> 1.3.5 110 | 111 | This runs build with correct version and ensures that when build is complete 112 | you will get no version change in git status. 113 | 114 | Since the common case is using ``git describe`` for actual version we have a 115 | shortcut for that: 116 | 117 | .. code-block:: console 118 | 119 | > bulk with-git-version your-build-command 120 | 1.3.5 -> 1.3.5-4-gd923e59-dirty 121 | [ .. output of your-build-command .. ] 122 | 1.3.5-4-gd923e59-dirty -> 1.3.5 123 | 124 | (the ``-dirty`` here means you have modified git-tracked files locally) 125 | 126 | .. note:: The ``git describe`` command is not strictly semver-compatible. 127 | I.e. the version ``x.y.z-n`` is treated as lower than ``x.y.z`` and you're 128 | supposed to use ``x.y.z+n`` for that. But for now we decided to stick to 129 | what ``git describe`` provides for now. We may provide an option to fix 130 | that in future, in the meantime you can use ``with-version``. 131 | 132 | 133 | Other Commands 134 | -------------- 135 | 136 | To check if version number is fine (consistent) run: 137 | 138 | .. code-block:: console 139 | 140 | > vagga bulk check-version 141 | setup.py:6: (v1.3.5) version='1.3.5', 142 | trafaret_config/__init__.py:1: (v1.3.5) __version__ = '1.3.5' 143 | 144 | It shows you files and lines where version number is present and will fail 145 | if there is no version at all or version is inconsistent between multiple 146 | files. 147 | 148 | .. note:: it will **not** show you files and lines which are present in config 149 | file but has no version number found. So when adding an entry in 150 | ``bulk.yaml`` you should run ``check-version`` and make sure the actual 151 | entry exists in the file. 152 | 153 | To fix inconsistent version run: 154 | 155 | .. code-block:: console 156 | 157 | > vagga bulk set-version v1.3.5 --force 158 | setup.py:6: (v1.3.4 -> v1.3.5) version='1.3.5', 159 | trafaret_config/__init__.py:1: (v1.2.3 -> v1.3.5) __version__ = '1.3.5' 160 | 161 | Same restriction for not found version as for ``check-version`` applies here. 162 | -------------------------------------------------------------------------------- /src/version.rs: -------------------------------------------------------------------------------- 1 | use std::str::CharIndices; 2 | use std::iter::{Peekable}; 3 | use std::cmp::Ordering; 4 | 5 | use argparse::FromCommandLine; 6 | 7 | #[derive(Debug, Clone, Deserialize)] 8 | pub struct Version>(pub T); 9 | pub struct Components<'a>(&'a str, Peekable>); 10 | 11 | #[derive(Debug, Eq, PartialEq, Clone)] 12 | pub enum Component<'a> { 13 | Numeric(u64), 14 | String(&'a str), 15 | } 16 | 17 | impl> ::std::fmt::Display for Version { 18 | fn fmt(&self, fmt: &mut ::std::fmt::Formatter) -> ::std::fmt::Result { 19 | self.0.as_ref().fmt(fmt) 20 | } 21 | } 22 | 23 | impl<'a> ::std::fmt::Display for Component<'a> { 24 | fn fmt(&self, fmt: &mut ::std::fmt::Formatter) -> ::std::fmt::Result { 25 | match *self { 26 | Component::Numeric(v) => v.fmt(fmt), 27 | Component::String(v) => v.fmt(fmt), 28 | } 29 | } 30 | } 31 | 32 | impl> AsRef for Version { 33 | fn as_ref(&self) -> &str { 34 | self.0.as_ref() 35 | } 36 | } 37 | 38 | impl> Version { 39 | pub fn num(&self) -> &str { 40 | let s = self.0.as_ref(); 41 | if s.starts_with("v") { 42 | &s[1..] 43 | } else { 44 | s 45 | } 46 | } 47 | pub fn components(&self) -> Components { 48 | let mut ch = self.0.as_ref().char_indices().peekable(); 49 | if ch.peek() == Some(&(0, 'v')) { 50 | ch.next(); 51 | } 52 | return Components(self.0.as_ref(), ch); 53 | } 54 | } 55 | 56 | 57 | impl<'a> Iterator for Components<'a> { 58 | type Item = Component<'a>; 59 | fn next(&mut self) -> Option> { 60 | use self::Component::*; 61 | while let Some(&(_, x)) = self.1.peek() { 62 | if x == '+' { 63 | // Ignore anything after +, i.e. treat it as end of string 64 | for _ in self.1.by_ref() {} 65 | return None; 66 | } 67 | if x.is_alphanumeric() { break; } 68 | self.1.next(); 69 | } 70 | if let Some(&(start, x)) = self.1.peek() { 71 | if x.is_numeric() { 72 | while let Some(&(_, x)) = self.1.peek() { 73 | if !x.is_numeric() { break; } 74 | self.1.next(); 75 | } 76 | let end = self.1.peek().map(|&(x, _)| x) 77 | .unwrap_or(self.0.len()); 78 | let val = &self.0[start..end]; 79 | return Some(val.parse().map(Numeric).unwrap_or(String(val))); 80 | } else { 81 | while let Some(&(_, x)) = self.1.peek() { 82 | if !x.is_alphanumeric() { break; } 83 | self.1.next(); 84 | } 85 | let end = self.1.peek().map(|&(x, _)| x) 86 | .unwrap_or(self.0.len()); 87 | let val = &self.0[start..end]; 88 | return Some(String(val)); 89 | } 90 | } 91 | None 92 | } 93 | } 94 | 95 | impl> PartialEq for Version { 96 | fn eq(&self, other: &Version) -> bool { 97 | self.cmp(other) == Ordering::Equal 98 | } 99 | } 100 | 101 | impl> Eq for Version {} 102 | 103 | impl> PartialOrd for Version { 104 | fn partial_cmp(&self, other: &Version) -> Option { 105 | Some(self.cmp(other)) 106 | } 107 | } 108 | 109 | impl> Ord for Version { 110 | fn cmp(&self, other: &Version) -> Ordering { 111 | use self::Component::*; 112 | use std::cmp::Ordering::*; 113 | let mut aiter = self.components(); 114 | let mut biter = other.components(); 115 | loop { 116 | let val = match (aiter.next(), biter.next()) { 117 | (Some(Numeric(x)), Some(Numeric(y))) => x.cmp(&y), 118 | (Some(Numeric(_)), Some(String(_))) => Greater, 119 | (Some(String(_)), Some(Numeric(_))) => Less, 120 | (Some(String(x)), Some(String(y))) => x.cmp(y), 121 | (Some(Numeric(_)), None) => Greater, 122 | (None, Some(Numeric(_))) => Less, 123 | (None, Some(String(x))) 124 | if matches!(x, "a"|"b"|"c"|"rc"|"pre"|"dev"|"dirty") 125 | => Greater, 126 | (None, Some(String(_))) => Less, 127 | (Some(String(x)), None) 128 | // git revision starts with g 129 | if matches!(x, "a"|"b"|"c"|"rc"|"pre"|"dev"|"dirty") 130 | || x.starts_with("g") 131 | => Less, 132 | (Some(String(_)), None) => Greater, 133 | (None, None) => return Equal, 134 | }; 135 | if val != Equal { 136 | return val; 137 | } 138 | } 139 | } 140 | } 141 | 142 | impl FromCommandLine for Version { 143 | fn from_argument(val: &str) -> Result, String> { 144 | Ok(Version(val.to_string())) 145 | } 146 | } 147 | 148 | #[cfg(test)] 149 | mod test { 150 | use super::Version; 151 | 152 | #[test] 153 | fn test_version_parse() { 154 | use super::Component::*; 155 | assert_eq!(Version("v0.4.1-28-gfba00d7") 156 | .components().collect::>(), 157 | [Numeric(0), Numeric(4), Numeric(1), 158 | Numeric(28), String("gfba00d7")]); 159 | assert_eq!(Version("v0.4.1+trusty1") 160 | .components().collect::>(), 161 | [Numeric(0), Numeric(4), Numeric(1)]); 162 | } 163 | 164 | #[test] 165 | fn test_version_cmp() { 166 | assert!(Version("v0.4.1-28-gfba00d7") > Version("v0.4.1")); 167 | assert!(Version("v0.4.1-28-gfba00d7") > Version("v0.4.1-27-gtest")); 168 | assert!(Version("v0.4.1-28-gfba00d7") < Version("v0.4.2")); 169 | assert!(Version("v0.4.1+trusty1") == Version("v0.4.1")); 170 | assert!(Version("v0.4.1+trusty1") == Version("v0.4.1+precise1")); 171 | } 172 | 173 | #[test] 174 | fn test_version_cmp2() { 175 | assert!(!(Version("v0.4.1-172-ge011471") 176 | < Version("v0.4.1-172-ge011471"))); 177 | } 178 | 179 | #[test] 180 | fn semver_test() { 181 | // example from semver.org 182 | assert!(Version("1.0.0-alpha") < Version("1.0.0-alpha.1")); 183 | // This one is intentionally doesn't work, beta is always lower 184 | // than digits (we use PEP 440) from python as it seems more reasonable 185 | // for parsing arbitrary version number rather than fairly strict 186 | // semver 187 | // assert!(Version("1.0.0-alpha.1") < Version("1.0.0-alpha.beta")); 188 | assert!(Version("1.0.0-alpha.beta") < Version("1.0.0-beta")); 189 | assert!(Version("1.0.0-beta") < Version("1.0.0-beta.2")); 190 | assert!(Version("1.0.0-beta.2") < Version("1.0.0-beta.11")); 191 | assert!(Version("1.0.0-beta.11") < Version("1.0.0-rc.1")); 192 | assert!(Version("1.0.0-rc.1") < Version("1.0.0")); 193 | } 194 | } 195 | -------------------------------------------------------------------------------- /src/ver/bump.rs: -------------------------------------------------------------------------------- 1 | use std::cmp::min; 2 | 3 | use time; 4 | 5 | use version::{Version, Component}; 6 | 7 | #[derive(Debug, Clone, Copy)] 8 | pub enum Bump { 9 | Patch, 10 | Minor, 11 | Major, 12 | Component(u8), 13 | DateMajor, 14 | } 15 | 16 | quick_error! { 17 | #[derive(Debug)] 18 | pub enum Error { 19 | NonNumericComponent(val: String) { 20 | display("component {:?} is non-numeric", val) 21 | } 22 | FutureDate(val: String) { 23 | display("current version {:?} represents date in future", val) 24 | } 25 | } 26 | } 27 | 28 | pub fn bump_version>(ver: &Version, how: Bump) 29 | -> Result, Error> 30 | { 31 | match how { 32 | Bump::Component(i) => { 33 | let mut result = Vec::new(); 34 | let mut iter = ver.components(); 35 | for n in iter.by_ref().take((i-1) as usize) { 36 | result.push(n); 37 | } 38 | while result.len() < (i-1) as usize { 39 | result.push(Component::Numeric(0)); 40 | } 41 | let n: u64 = match iter.next() { 42 | Some(Component::Numeric(x)) => x+1, 43 | Some(Component::String(x)) => { 44 | return Err(Error::NonNumericComponent(x.into())); 45 | }, 46 | None => 1, 47 | }; 48 | result.push(Component::Numeric(n)); 49 | while result.len() < 3 { 50 | result.push(Component::Numeric(0)); 51 | } 52 | let mut buf = format!("v{}", result[0]); 53 | for i in &result[1..] { 54 | use std::fmt::Write; 55 | write!(&mut buf, ".{}", i).unwrap(); 56 | } 57 | Ok(Version(buf)) 58 | } 59 | Bump::Major|Bump::Minor|Bump::Patch => { 60 | let idx = ver.components() 61 | .position(|x| matches!(x, Component::Numeric(y) if y > 0)) 62 | // If version is v0.0.0 we consider major bump is v0.0.1 63 | .unwrap_or(2) 64 | +1; // 1-based index 65 | let cmp = match (idx, how) { 66 | (idx, Bump::Major) => min(idx, 3), 67 | (1, Bump::Minor) => 2, 68 | (_, _) => 3, 69 | }; 70 | return bump_version(ver, Bump::Component(cmp as u8)); 71 | } 72 | Bump::DateMajor => { 73 | let tm = time::now_utc(); 74 | let new_ver = ((tm.tm_year - 100) * 10000 + 75 | (tm.tm_mon+1) * 100 + 76 | tm.tm_mday) as u64; 77 | let mut components = ver.components(); 78 | match components.next() { 79 | Some(Component::Numeric(x)) if x == new_ver => {} 80 | Some(Component::Numeric(x)) if x >= new_ver => { 81 | return Err(Error::FutureDate(ver.as_ref().to_string())); 82 | } 83 | Some(Component::Numeric(_)) | 84 | None => { 85 | return Ok(Version(format!("v{}.0", new_ver))); 86 | } 87 | Some(Component::String(x)) => { 88 | return Err(Error::NonNumericComponent(x.into())); 89 | }, 90 | } 91 | let n: u64 = match components.next() { 92 | Some(Component::Numeric(x)) => x+1, 93 | Some(Component::String(x)) => { 94 | return Err(Error::NonNumericComponent(x.into())); 95 | }, 96 | None => 1, 97 | }; 98 | return Ok(Version(format!("v{}.{}", new_ver, n))); 99 | } 100 | } 101 | } 102 | 103 | #[cfg(test)] 104 | mod test { 105 | use time; 106 | use super::bump_version; 107 | use super::Bump; 108 | use super::Bump::*; 109 | use version::Version; 110 | 111 | fn bump_component(ver: &str, bump: u8) -> String { 112 | format!("{}", 113 | bump_version(&Version(ver), Bump::Component(bump)) 114 | .unwrap()) 115 | } 116 | fn bump_semver(ver: &str, bump: Bump) -> String { 117 | format!("{}", 118 | bump_version(&Version(ver), bump) 119 | .unwrap()) 120 | } 121 | fn bump_date(ver: &str) -> String { 122 | format!("{}", 123 | bump_version(&Version(ver), DateMajor) 124 | .unwrap()) 125 | } 126 | #[test] 127 | fn test_cmp1() { 128 | assert_eq!(bump_component("v1.2.0", 1), "v2.0.0"); 129 | assert_eq!(bump_component("v0.0.0", 1), "v1.0.0"); 130 | assert_eq!(bump_component("v0", 1), "v1.0.0"); 131 | assert_eq!(bump_component("v7.38.96", 1), "v8.0.0"); 132 | assert_eq!(bump_component("v9.38.96", 1), "v10.0.0"); 133 | assert_eq!(bump_component("v12.38.96", 1), "v13.0.0"); 134 | } 135 | #[test] 136 | fn test_cmp2() { 137 | assert_eq!(bump_component("v1.2.0", 2), "v1.3.0"); 138 | assert_eq!(bump_component("v0.0.0", 2), "v0.1.0"); 139 | assert_eq!(bump_component("v0", 2), "v0.1.0"); 140 | assert_eq!(bump_component("v7.38.96", 2), "v7.39.0"); 141 | assert_eq!(bump_component("v7.9.96", 2), "v7.10.0"); 142 | assert_eq!(bump_component("v12.38.96", 2), "v12.39.0"); 143 | } 144 | #[test] 145 | fn test_cmp3() { 146 | assert_eq!(bump_component("v1.2.0", 3), "v1.2.1"); 147 | assert_eq!(bump_component("v0.0.0", 3), "v0.0.1"); 148 | assert_eq!(bump_component("v0", 3), "v0.0.1"); 149 | assert_eq!(bump_component("v7.38.96", 3), "v7.38.97"); 150 | assert_eq!(bump_component("v7.13.99", 3), "v7.13.100"); 151 | } 152 | 153 | #[test] 154 | fn test_major() { 155 | assert_eq!(bump_semver("v1.2.0", Major), "v2.0.0"); 156 | assert_eq!(bump_semver("v0.0.0", Major), "v0.0.1"); 157 | assert_eq!(bump_semver("v0.0.99", Major), "v0.0.100"); 158 | assert_eq!(bump_semver("v0", Major), "v0.0.1"); 159 | assert_eq!(bump_semver("v7.38.96", Major), "v8.0.0"); 160 | assert_eq!(bump_semver("v9.38.96", Major), "v10.0.0"); 161 | assert_eq!(bump_semver("v12.38.96", Major), "v13.0.0"); 162 | assert_eq!(bump_semver("v0.3.7", Major), "v0.4.0"); 163 | assert_eq!(bump_semver("v0.9.17", Major), "v0.10.0"); 164 | } 165 | #[test] 166 | fn test_minor() { 167 | assert_eq!(bump_semver("v1.2.0", Minor), "v1.3.0"); 168 | assert_eq!(bump_semver("v0.0.0", Minor), "v0.0.1"); 169 | assert_eq!(bump_semver("v0.0.99", Minor), "v0.0.100"); 170 | assert_eq!(bump_semver("v0", Minor), "v0.0.1"); 171 | assert_eq!(bump_semver("v7.38.96", Minor), "v7.39.0"); 172 | assert_eq!(bump_semver("v9.38.96", Minor), "v9.39.0"); 173 | assert_eq!(bump_semver("v12.38.96", Minor), "v12.39.0"); 174 | assert_eq!(bump_semver("v0.3.7", Minor), "v0.3.8"); 175 | assert_eq!(bump_semver("v0.9.17", Minor), "v0.9.18"); 176 | } 177 | 178 | #[test] 179 | fn test_patch() { 180 | assert_eq!(bump_semver("v1.2.0", Patch), "v1.2.1"); 181 | assert_eq!(bump_semver("v0.0.0", Patch), "v0.0.1"); 182 | assert_eq!(bump_semver("v0.0.99", Patch), "v0.0.100"); 183 | assert_eq!(bump_semver("v0", Patch), "v0.0.1"); 184 | assert_eq!(bump_semver("v7.38.96", Patch), "v7.38.97"); 185 | assert_eq!(bump_semver("v7.13.99", Patch), "v7.13.100"); 186 | } 187 | #[test] 188 | fn test_date() { 189 | let dest = time::strftime("v%y%m%d.0", &time::now_utc()).unwrap(); 190 | let dest1 = time::strftime("v%y%m%d.1", &time::now_utc()).unwrap(); 191 | let dest2 = time::strftime("v%y%m%d.2", &time::now_utc()).unwrap(); 192 | let dest9 = time::strftime("v%y%m%d.9", &time::now_utc()).unwrap(); 193 | let dest10 = time::strftime("v%y%m%d.10", &time::now_utc()).unwrap(); 194 | let dest11 = time::strftime("v%y%m%d.11", &time::now_utc()).unwrap(); 195 | assert_eq!(bump_date("v1.2.0"), dest); 196 | assert_eq!(bump_date(&dest), dest1); 197 | assert_eq!(bump_date(&dest1), dest2); 198 | assert_eq!(bump_date(&dest9), dest10); 199 | assert_eq!(bump_date(&dest10), dest11); 200 | } 201 | } 202 | -------------------------------------------------------------------------------- /doc/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = _build 9 | 10 | # Internal variables. 11 | PAPEROPT_a4 = -D latex_paper_size=a4 12 | PAPEROPT_letter = -D latex_paper_size=letter 13 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 14 | # the i18n builder cannot share the environment and doctrees with the others 15 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 16 | 17 | .PHONY: help 18 | help: 19 | @echo "Please use \`make ' where is one of" 20 | @echo " html to make standalone HTML files" 21 | @echo " dirhtml to make HTML files named index.html in directories" 22 | @echo " singlehtml to make a single large HTML file" 23 | @echo " pickle to make pickle files" 24 | @echo " json to make JSON files" 25 | @echo " htmlhelp to make HTML files and a HTML help project" 26 | @echo " qthelp to make HTML files and a qthelp project" 27 | @echo " applehelp to make an Apple Help Book" 28 | @echo " devhelp to make HTML files and a Devhelp project" 29 | @echo " epub to make an epub" 30 | @echo " epub3 to make an epub3" 31 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 32 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 33 | @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" 34 | @echo " text to make text files" 35 | @echo " man to make manual pages" 36 | @echo " texinfo to make Texinfo files" 37 | @echo " info to make Texinfo files and run them through makeinfo" 38 | @echo " gettext to make PO message catalogs" 39 | @echo " changes to make an overview of all changed/added/deprecated items" 40 | @echo " xml to make Docutils-native XML files" 41 | @echo " pseudoxml to make pseudoxml-XML files for display purposes" 42 | @echo " linkcheck to check all external links for integrity" 43 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 44 | @echo " coverage to run coverage check of the documentation (if enabled)" 45 | @echo " dummy to check syntax errors of document sources" 46 | 47 | .PHONY: clean 48 | clean: 49 | rm -rf $(BUILDDIR)/* 50 | 51 | .PHONY: html 52 | html: 53 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 54 | @echo 55 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 56 | 57 | .PHONY: dirhtml 58 | dirhtml: 59 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 60 | @echo 61 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 62 | 63 | .PHONY: singlehtml 64 | singlehtml: 65 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 66 | @echo 67 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 68 | 69 | .PHONY: pickle 70 | pickle: 71 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 72 | @echo 73 | @echo "Build finished; now you can process the pickle files." 74 | 75 | .PHONY: json 76 | json: 77 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 78 | @echo 79 | @echo "Build finished; now you can process the JSON files." 80 | 81 | .PHONY: htmlhelp 82 | htmlhelp: 83 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 84 | @echo 85 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 86 | ".hhp project file in $(BUILDDIR)/htmlhelp." 87 | 88 | .PHONY: qthelp 89 | qthelp: 90 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 91 | @echo 92 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 93 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 94 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/Bulk.qhcp" 95 | @echo "To view the help file:" 96 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/Bulk.qhc" 97 | 98 | .PHONY: applehelp 99 | applehelp: 100 | $(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp 101 | @echo 102 | @echo "Build finished. The help book is in $(BUILDDIR)/applehelp." 103 | @echo "N.B. You won't be able to view it unless you put it in" \ 104 | "~/Library/Documentation/Help or install it in your application" \ 105 | "bundle." 106 | 107 | .PHONY: devhelp 108 | devhelp: 109 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 110 | @echo 111 | @echo "Build finished." 112 | @echo "To view the help file:" 113 | @echo "# mkdir -p $$HOME/.local/share/devhelp/Bulk" 114 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/Bulk" 115 | @echo "# devhelp" 116 | 117 | .PHONY: epub 118 | epub: 119 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 120 | @echo 121 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 122 | 123 | .PHONY: epub3 124 | epub3: 125 | $(SPHINXBUILD) -b epub3 $(ALLSPHINXOPTS) $(BUILDDIR)/epub3 126 | @echo 127 | @echo "Build finished. The epub3 file is in $(BUILDDIR)/epub3." 128 | 129 | .PHONY: latex 130 | latex: 131 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 132 | @echo 133 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 134 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 135 | "(use \`make latexpdf' here to do that automatically)." 136 | 137 | .PHONY: latexpdf 138 | latexpdf: 139 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 140 | @echo "Running LaTeX files through pdflatex..." 141 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 142 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 143 | 144 | .PHONY: latexpdfja 145 | latexpdfja: 146 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 147 | @echo "Running LaTeX files through platex and dvipdfmx..." 148 | $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja 149 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 150 | 151 | .PHONY: text 152 | text: 153 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 154 | @echo 155 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 156 | 157 | .PHONY: man 158 | man: 159 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 160 | @echo 161 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 162 | 163 | .PHONY: texinfo 164 | texinfo: 165 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 166 | @echo 167 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 168 | @echo "Run \`make' in that directory to run these through makeinfo" \ 169 | "(use \`make info' here to do that automatically)." 170 | 171 | .PHONY: info 172 | info: 173 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 174 | @echo "Running Texinfo files through makeinfo..." 175 | make -C $(BUILDDIR)/texinfo info 176 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 177 | 178 | .PHONY: gettext 179 | gettext: 180 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 181 | @echo 182 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 183 | 184 | .PHONY: changes 185 | changes: 186 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 187 | @echo 188 | @echo "The overview file is in $(BUILDDIR)/changes." 189 | 190 | .PHONY: linkcheck 191 | linkcheck: 192 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 193 | @echo 194 | @echo "Link check complete; look for any errors in the above output " \ 195 | "or in $(BUILDDIR)/linkcheck/output.txt." 196 | 197 | .PHONY: doctest 198 | doctest: 199 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 200 | @echo "Testing of doctests in the sources finished, look at the " \ 201 | "results in $(BUILDDIR)/doctest/output.txt." 202 | 203 | .PHONY: coverage 204 | coverage: 205 | $(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage 206 | @echo "Testing of coverage in the sources finished, look at the " \ 207 | "results in $(BUILDDIR)/coverage/python.txt." 208 | 209 | .PHONY: xml 210 | xml: 211 | $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml 212 | @echo 213 | @echo "Build finished. The XML files are in $(BUILDDIR)/xml." 214 | 215 | .PHONY: pseudoxml 216 | pseudoxml: 217 | $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml 218 | @echo 219 | @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." 220 | 221 | .PHONY: dummy 222 | dummy: 223 | $(SPHINXBUILD) -b dummy $(ALLSPHINXOPTS) $(BUILDDIR)/dummy 224 | @echo 225 | @echo "Build finished. Dummy builder generates no files." 226 | -------------------------------------------------------------------------------- /src/ver/commit.rs: -------------------------------------------------------------------------------- 1 | use std::collections::{VecDeque, HashMap}; 2 | use std::env; 3 | use std::error::Error; 4 | use std::io::{self, stderr, Write, BufWriter}; 5 | use std::io::{Seek, SeekFrom, BufReader, BufRead}; 6 | use std::path::Path; 7 | use std::process::Command; 8 | 9 | use git2::{Repository, Status, Index, Commit}; 10 | use tempfile::{NamedTempFile, Builder}; 11 | 12 | use config::{Config}; 13 | use version::{Version}; 14 | 15 | 16 | 17 | pub fn check_status(cfg: &Config, dir: &Path) 18 | -> Result> 19 | { 20 | let repo = Repository::open(".")?; 21 | 22 | // check config 23 | let git_config = repo.config()?; 24 | git_config.get_entry("user.name")?; 25 | git_config.get_entry("user.email")?; 26 | 27 | // check status of files 28 | let mut ok = true; 29 | for item in &cfg.versions { 30 | for filename in item.file.iter().chain(&item.files) { 31 | let path = dir.join(filename); 32 | // git don't like ./ paths 33 | let git_path = path.strip_prefix(".").unwrap_or(&path); 34 | let status = repo.status_file(git_path)?; 35 | if status != Status::empty() { 36 | writeln!(&mut stderr(), "File {:?} is dirty", filename).ok(); 37 | ok = false; 38 | } 39 | } 40 | } 41 | if !ok { 42 | return Err(format!("all files with version number must be unchanged \ 43 | before bumping version").into()); 44 | } 45 | 46 | Ok(repo) 47 | } 48 | 49 | fn message_file(repo: &Repository, ver: &Version, commit: Commit, 50 | initial_tag: Option) 51 | -> Result> 52 | { 53 | let mut file = Builder::new() 54 | .suffix(".TAG_COMMIT") 55 | .tempfile()?; 56 | { 57 | let mut buf = BufWriter::new(&mut file); 58 | writeln!(&mut buf, "Version v{}: ", ver.num())?; 59 | writeln!(&mut buf, "#")?; 60 | writeln!(&mut buf, "# Write a message for tag:")?; 61 | writeln!(&mut buf, "# v{}", ver.num())?; 62 | writeln!(&mut buf, "# Lines starting with '#' will be ignored.")?; 63 | writeln!(&mut buf, "#")?; 64 | writeln!(&mut buf, "# Log:")?; 65 | 66 | let tag_names = repo.tag_names(Some("v*"))?; 67 | let tags = tag_names.iter() 68 | .filter_map(|name| name) 69 | .filter_map(|name| 70 | repo.refname_to_id(&format!("refs/tags/{}", name)).ok() 71 | .and_then(|oid| repo.find_tag(oid).ok()) 72 | .map(|tag| (tag.target_id(), name))) 73 | .collect::>(); 74 | 75 | let mut queue = VecDeque::new(); 76 | queue.push_back(commit); 77 | for _ in 0..100 { 78 | let commit = match queue.pop_front() { 79 | Some(x) => x, 80 | None => break, 81 | }; 82 | let msg = commit.message() 83 | .and_then(|x| x.lines().next()) 84 | .unwrap_or(""); 85 | if let Some(tag_name) = tags.get(&commit.id()) { 86 | writeln!(&mut buf, "# {:0.8} [tag: {}] {}", 87 | commit.id(), tag_name, msg)?; 88 | if let Some(ref init_tag) = initial_tag { 89 | if init_tag == tag_name { 90 | break; 91 | } 92 | } else { 93 | break; 94 | } 95 | } else { 96 | writeln!(&mut buf, "# {:0.8} {}", commit.id(), msg)?; 97 | } 98 | for pid in commit.parent_ids() { 99 | queue.push_back(repo.find_commit(pid)?); 100 | } 101 | } 102 | } 103 | Ok(file) 104 | } 105 | 106 | fn spawn_editor(file_name: &Path) -> Result<(), Box> { 107 | if let Some(editor) = env::var_os("VISUAL") { 108 | let mut cmd = Command::new(editor); 109 | cmd.arg(file_name); 110 | match cmd.status() { 111 | Ok(s) if s.success() => return Ok(()), 112 | Ok(s) => return Err(format!("editor exited with {}", s).into()), 113 | Err(ref e) if e.kind() == io::ErrorKind::NotFound => { 114 | } 115 | Err(e) => return Err(e.into()), 116 | } 117 | } 118 | if let Some(editor) = env::var_os("EDITOR") { 119 | let mut cmd = Command::new(editor); 120 | cmd.arg(file_name); 121 | match cmd.status() { 122 | Ok(s) if s.success() => return Ok(()), 123 | Ok(s) => return Err(format!("editor exited with {}", s).into()), 124 | Err(ref e) if e.kind() == io::ErrorKind::NotFound => { 125 | } 126 | Err(e) => return Err(e.into()), 127 | } 128 | } 129 | let mut cmd = Command::new("vim"); 130 | cmd.arg(file_name); 131 | match cmd.status() { 132 | Ok(s) if s.success() => return Ok(()), 133 | Ok(s) => return Err(format!("vim exited with {}", s).into()), 134 | Err(ref e) if e.kind() == io::ErrorKind::NotFound => { 135 | } 136 | Err(e) => return Err(e.into()), 137 | } 138 | let mut cmd = Command::new("vi"); 139 | cmd.arg(file_name); 140 | match cmd.status() { 141 | Ok(s) if s.success() => return Ok(()), 142 | Ok(s) => return Err(format!("vi exited with {}", s).into()), 143 | Err(ref e) if e.kind() == io::ErrorKind::NotFound => { 144 | } 145 | Err(e) => return Err(e.into()), 146 | } 147 | let mut cmd = Command::new("nano"); 148 | cmd.arg(file_name); 149 | match cmd.status() { 150 | Ok(s) if s.success() => return Ok(()), 151 | Ok(s) => return Err(format!("nano exited with {}", s).into()), 152 | Err(ref e) if e.kind() == io::ErrorKind::NotFound => { 153 | } 154 | Err(e) => return Err(e.into()), 155 | } 156 | Err(format!("no editor found").into()) 157 | } 158 | 159 | pub fn commit_version(cfg: &Config, dir: &Path, repo: &mut Repository, 160 | ver: &Version, original_version: Option<&Version>, 161 | dry_run: bool) 162 | -> Result<(), Box> 163 | { 164 | let mut file_index = repo.index()?; 165 | // use temporary index so that we don't bother with things in user's index 166 | let mut index = Index::new()?; 167 | let head = repo.head()?; 168 | let head_oid = head.resolve()?.target() 169 | .ok_or(format!("can't resolve head"))?; 170 | let head_commit = repo.find_commit(head_oid)?; 171 | let head_tree = repo.find_tree(head_commit.tree_id())?; 172 | index.read_tree(&head_tree)?; 173 | repo.set_index(&mut index); 174 | for item in &cfg.versions { 175 | for filename in item.file.iter().chain(&item.files) { 176 | let path = dir.join(filename); 177 | // git don't like ./ paths 178 | let git_path = path.strip_prefix(".").unwrap_or(&path); 179 | index.add_path(&git_path)?; 180 | } 181 | } 182 | if !dry_run { 183 | let tree_oid = index.write_tree()?; 184 | let sig = repo.signature()?; 185 | let tree = repo.find_tree(tree_oid)?; 186 | let oid = repo.commit(Some("HEAD"), &sig, &sig, 187 | &format!("Version bumped to v{}", ver.num()), 188 | &tree, &[&head_commit])?; 189 | println!("Commited as {}", oid); 190 | let commit_ob = repo.find_object(oid, None)?; 191 | 192 | // then update user's index 193 | repo.set_index(&mut file_index); 194 | for item in &cfg.versions { 195 | for filename in item.file.iter().chain(&item.files) { 196 | let path = dir.join(filename); 197 | // git don't like ./ paths 198 | let git_path = path.strip_prefix(".").unwrap_or(&path); 199 | file_index.add_path(&git_path)?; 200 | } 201 | } 202 | file_index.write()?; 203 | 204 | let commit = repo.find_commit(oid)?; 205 | let mut message_file = message_file(repo, ver, commit, 206 | original_version.map(|x| format!("v{}", x.num())))?; 207 | spawn_editor(message_file.path())?; 208 | message_file.seek(SeekFrom::Start(0))?; 209 | let mut message = String::with_capacity(512); 210 | for line in BufReader::new(message_file).lines() { 211 | let line = line?; 212 | if !line.starts_with("#") { 213 | message.push_str(line.trim_right()); 214 | message.push('\n'); 215 | } 216 | } 217 | if message.trim() == "" { 218 | return Err("tag description is empty, \ 219 | aborting tag creation.".into()) 220 | } 221 | 222 | repo.tag(&format!("v{}", ver.num()), 223 | &commit_ob, &sig, 224 | &message.trim(), 225 | false)?; 226 | println!("Created tag v{}", ver.num()); 227 | println!("To push tag run:"); 228 | println!(" git push --atomic origin HEAD v{}", ver.num()); 229 | } 230 | Ok(()) 231 | } 232 | -------------------------------------------------------------------------------- /doc/conf.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # 4 | # Bulk documentation build configuration file, created by 5 | # sphinx-quickstart on Fri Jun 10 19:30:43 2016. 6 | # 7 | # This file is execfile()d with the current directory set to its 8 | # containing dir. 9 | # 10 | # Note that not all possible configuration values are present in this 11 | # autogenerated file. 12 | # 13 | # All configuration values have a default; values that are commented out 14 | # serve to show the default. 15 | 16 | # If extensions (or modules to document with autodoc) are in another directory, 17 | # add these directories to sys.path here. If the directory is relative to the 18 | # documentation root, use os.path.abspath to make it absolute, like shown here. 19 | # 20 | # import os 21 | # import sys 22 | # sys.path.insert(0, os.path.abspath('.')) 23 | 24 | # -- General configuration ------------------------------------------------ 25 | 26 | # If your documentation needs a minimal Sphinx version, state it here. 27 | # 28 | # needs_sphinx = '1.0' 29 | 30 | # Add any Sphinx extension module names here, as strings. They can be 31 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 32 | # ones. 33 | extensions = [] 34 | 35 | # Add any paths that contain templates here, relative to this directory. 36 | templates_path = ['_templates'] 37 | 38 | # The suffix(es) of source filenames. 39 | # You can specify multiple suffix as a list of string: 40 | # 41 | # source_suffix = ['.rst', '.md'] 42 | source_suffix = '.rst' 43 | 44 | # The encoding of source files. 45 | # 46 | # source_encoding = 'utf-8-sig' 47 | 48 | # The master toctree document. 49 | master_doc = 'index' 50 | 51 | # General information about the project. 52 | project = 'Bulk' 53 | copyright = '2016, paul@colomiets.name' 54 | author = 'paul@colomiets.name' 55 | 56 | # The version info for the project you're documenting, acts as replacement for 57 | # |version| and |release|, also used in various other places throughout the 58 | # built documents. 59 | # 60 | # The short X.Y version. 61 | version = '0.4' 62 | # The full version, including alpha/beta/rc tags. 63 | release = '0.4.2' 64 | 65 | # The language for content autogenerated by Sphinx. Refer to documentation 66 | # for a list of supported languages. 67 | # 68 | # This is also used if you do content translation via gettext catalogs. 69 | # Usually you set "language" from the command line for these cases. 70 | language = None 71 | 72 | # There are two options for replacing |today|: either, you set today to some 73 | # non-false value, then it is used: 74 | # 75 | # today = '' 76 | # 77 | # Else, today_fmt is used as the format for a strftime call. 78 | # 79 | # today_fmt = '%B %d, %Y' 80 | 81 | # List of patterns, relative to source directory, that match files and 82 | # directories to ignore when looking for source files. 83 | # This patterns also effect to html_static_path and html_extra_path 84 | exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] 85 | 86 | # The reST default role (used for this markup: `text`) to use for all 87 | # documents. 88 | # 89 | # default_role = None 90 | 91 | # If true, '()' will be appended to :func: etc. cross-reference text. 92 | # 93 | # add_function_parentheses = True 94 | 95 | # If true, the current module name will be prepended to all description 96 | # unit titles (such as .. function::). 97 | # 98 | # add_module_names = True 99 | 100 | # If true, sectionauthor and moduleauthor directives will be shown in the 101 | # output. They are ignored by default. 102 | # 103 | # show_authors = False 104 | 105 | # The name of the Pygments (syntax highlighting) style to use. 106 | pygments_style = 'sphinx' 107 | 108 | # A list of ignored prefixes for module index sorting. 109 | # modindex_common_prefix = [] 110 | 111 | # If true, keep warnings as "system message" paragraphs in the built documents. 112 | # keep_warnings = False 113 | 114 | # If true, `todo` and `todoList` produce output, else they produce nothing. 115 | todo_include_todos = False 116 | 117 | 118 | # -- Options for HTML output ---------------------------------------------- 119 | 120 | # The theme to use for HTML and HTML Help pages. See the documentation for 121 | # a list of builtin themes. 122 | # 123 | html_theme = 'alabaster' 124 | 125 | # Theme options are theme-specific and customize the look and feel of a theme 126 | # further. For a list of options available for each theme, see the 127 | # documentation. 128 | # 129 | # html_theme_options = {} 130 | 131 | # Add any paths that contain custom themes here, relative to this directory. 132 | # html_theme_path = [] 133 | 134 | # The name for this set of Sphinx documents. 135 | # " v documentation" by default. 136 | # 137 | # html_title = 'Bulk v0.4.2' 138 | 139 | # A shorter title for the navigation bar. Default is the same as html_title. 140 | # 141 | # html_short_title = None 142 | 143 | # The name of an image file (relative to this directory) to place at the top 144 | # of the sidebar. 145 | # 146 | # html_logo = None 147 | 148 | # The name of an image file (relative to this directory) to use as a favicon of 149 | # the docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 150 | # pixels large. 151 | # 152 | # html_favicon = None 153 | 154 | # Add any paths that contain custom static files (such as style sheets) here, 155 | # relative to this directory. They are copied after the builtin static files, 156 | # so a file named "default.css" will overwrite the builtin "default.css". 157 | html_static_path = ['_static'] 158 | 159 | # Add any extra paths that contain custom files (such as robots.txt or 160 | # .htaccess) here, relative to this directory. These files are copied 161 | # directly to the root of the documentation. 162 | # 163 | # html_extra_path = [] 164 | 165 | # If not None, a 'Last updated on:' timestamp is inserted at every page 166 | # bottom, using the given strftime format. 167 | # The empty string is equivalent to '%b %d, %Y'. 168 | # 169 | # html_last_updated_fmt = None 170 | 171 | # If true, SmartyPants will be used to convert quotes and dashes to 172 | # typographically correct entities. 173 | # 174 | # html_use_smartypants = True 175 | 176 | # Custom sidebar templates, maps document names to template names. 177 | # 178 | # html_sidebars = {} 179 | 180 | # Additional templates that should be rendered to pages, maps page names to 181 | # template names. 182 | # 183 | # html_additional_pages = {} 184 | 185 | # If false, no module index is generated. 186 | # 187 | # html_domain_indices = True 188 | 189 | # If false, no index is generated. 190 | # 191 | # html_use_index = True 192 | 193 | # If true, the index is split into individual pages for each letter. 194 | # 195 | # html_split_index = False 196 | 197 | # If true, links to the reST sources are added to the pages. 198 | # 199 | # html_show_sourcelink = True 200 | 201 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 202 | # 203 | # html_show_sphinx = True 204 | 205 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 206 | # 207 | # html_show_copyright = True 208 | 209 | # If true, an OpenSearch description file will be output, and all pages will 210 | # contain a tag referring to it. The value of this option must be the 211 | # base URL from which the finished HTML is served. 212 | # 213 | # html_use_opensearch = '' 214 | 215 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 216 | # html_file_suffix = None 217 | 218 | # Language to be used for generating the HTML full-text search index. 219 | # Sphinx supports the following languages: 220 | # 'da', 'de', 'en', 'es', 'fi', 'fr', 'h', 'it', 'ja' 221 | # 'nl', 'no', 'pt', 'ro', 'r', 'sv', 'tr', 'zh' 222 | # 223 | # html_search_language = 'en' 224 | 225 | # A dictionary with options for the search language support, empty by default. 226 | # 'ja' uses this config value. 227 | # 'zh' user can custom change `jieba` dictionary path. 228 | # 229 | # html_search_options = {'type': 'default'} 230 | 231 | # The name of a javascript file (relative to the configuration directory) that 232 | # implements a search results scorer. If empty, the default will be used. 233 | # 234 | # html_search_scorer = 'scorer.js' 235 | 236 | # Output file base name for HTML help builder. 237 | htmlhelp_basename = 'Bulkdoc' 238 | 239 | # -- Options for LaTeX output --------------------------------------------- 240 | 241 | latex_elements = { 242 | # The paper size ('letterpaper' or 'a4paper'). 243 | # 244 | # 'papersize': 'letterpaper', 245 | 246 | # The font size ('10pt', '11pt' or '12pt'). 247 | # 248 | # 'pointsize': '10pt', 249 | 250 | # Additional stuff for the LaTeX preamble. 251 | # 252 | # 'preamble': '', 253 | 254 | # Latex figure (float) alignment 255 | # 256 | # 'figure_align': 'htbp', 257 | } 258 | 259 | # Grouping the document tree into LaTeX files. List of tuples 260 | # (source start file, target name, title, 261 | # author, documentclass [howto, manual, or own class]). 262 | latex_documents = [ 263 | (master_doc, 'Bulk.tex', 'Bulk Documentation', 264 | 'paul@colomiets.name', 'manual'), 265 | ] 266 | 267 | # The name of an image file (relative to this directory) to place at the top of 268 | # the title page. 269 | # 270 | # latex_logo = None 271 | 272 | # For "manual" documents, if this is true, then toplevel headings are parts, 273 | # not chapters. 274 | # 275 | # latex_use_parts = False 276 | 277 | # If true, show page references after internal links. 278 | # 279 | # latex_show_pagerefs = False 280 | 281 | # If true, show URL addresses after external links. 282 | # 283 | # latex_show_urls = False 284 | 285 | # Documents to append as an appendix to all manuals. 286 | # 287 | # latex_appendices = [] 288 | 289 | # If false, no module index is generated. 290 | # 291 | # latex_domain_indices = True 292 | 293 | 294 | # -- Options for manual page output --------------------------------------- 295 | 296 | # One entry per manual page. List of tuples 297 | # (source start file, name, description, authors, manual section). 298 | man_pages = [ 299 | (master_doc, 'bulk', 'Bulk Documentation', 300 | [author], 1) 301 | ] 302 | 303 | # If true, show URL addresses after external links. 304 | # 305 | # man_show_urls = False 306 | 307 | 308 | # -- Options for Texinfo output ------------------------------------------- 309 | 310 | # Grouping the document tree into Texinfo files. List of tuples 311 | # (source start file, target name, title, author, 312 | # dir menu entry, description, category) 313 | texinfo_documents = [ 314 | (master_doc, 'Bulk', 'Bulk Documentation', 315 | author, 'Bulk', 'One line description of project.', 316 | 'Miscellaneous'), 317 | ] 318 | 319 | # Documents to append as an appendix to all manuals. 320 | # 321 | # texinfo_appendices = [] 322 | 323 | # If false, no module index is generated. 324 | # 325 | # texinfo_domain_indices = True 326 | 327 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 328 | # 329 | # texinfo_show_urls = 'footnote' 330 | 331 | # If true, do not generate a @detailmenu in the "Top" node's menu. 332 | # 333 | # texinfo_no_detailmenu = False 334 | -------------------------------------------------------------------------------- /LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | 203 | -------------------------------------------------------------------------------- /src/repo/debian.rs: -------------------------------------------------------------------------------- 1 | use std::io::{self, Write, BufWriter}; 2 | use std::fs::{File, create_dir_all, rename, copy, metadata}; 3 | use std::num::ParseIntError; 4 | use std::path::{PathBuf, Path}; 5 | use std::collections::{BTreeSet, BTreeMap, HashMap}; 6 | 7 | use time::now_utc; 8 | use sha2::{Sha256, Digest}; 9 | use unicase::UniCase; 10 | use quick_error::ResultExt; 11 | 12 | use version::Version; 13 | use hash_file::hash_file; 14 | use deb_ext::WriteDebExt; 15 | use repo::metadata::PackageMeta; 16 | use repo::deb::parse_control; 17 | 18 | 19 | #[derive(Debug)] 20 | pub struct Release { 21 | codename: String, 22 | architectures: BTreeSet, 23 | components: BTreeSet, 24 | sha256: BTreeMap, 25 | } 26 | 27 | #[derive(Debug)] 28 | pub struct Package { 29 | name: String, 30 | version: Version, 31 | architecture: String, 32 | filename: PathBuf, 33 | size: u64, 34 | sha256: String, 35 | metadata: BTreeMap, String>, 36 | } 37 | 38 | #[derive(Debug)] 39 | pub struct Packages(BTreeMap<(String, String), 40 | BTreeMap, Package>>); 41 | 42 | #[derive(Debug)] 43 | struct FileInfo { 44 | path: PathBuf, 45 | sha256: String, 46 | size: u64, 47 | } 48 | 49 | #[derive(Debug)] 50 | pub struct Component<'a>(&'a mut Packages, 51 | &'a mut HashMap); 52 | 53 | #[derive(Debug)] 54 | pub struct Repository { 55 | root: PathBuf, 56 | suites: HashMap, 57 | components: HashMap<(String, String, String), Packages>, 58 | files: HashMap, 59 | } 60 | 61 | #[derive(Debug, Clone, Copy)] 62 | pub enum ConflictResolution { 63 | Error, 64 | Keep, 65 | Replace, 66 | } 67 | 68 | quick_error! { 69 | #[derive(Debug)] 70 | pub enum ReleaseFileRead { 71 | Io(err: io::Error) { 72 | from() 73 | description("io error") 74 | display("io error: {}", err) 75 | } 76 | AbsentField(field: &'static str) { 77 | description("required field is absent") 78 | display("field {:?} is absent", field) 79 | } 80 | ExcessiveControlData { 81 | description("more than one control data in Releases file") 82 | } 83 | FileSize(err: ParseIntError) { 84 | from() 85 | display("error parsing file size: {}", err) 86 | description("error parsing file size") 87 | } 88 | InvalidHashLine { 89 | description("one of the lines of SHA256 has invalid format") 90 | } 91 | } 92 | } 93 | 94 | quick_error! { 95 | #[derive(Debug)] 96 | pub enum PackagesRead { 97 | Io(err: io::Error) { 98 | from() 99 | description("io error") 100 | display("io error: {}", err) 101 | } 102 | AbsentField(field: &'static str) { 103 | description("required field is absent") 104 | display("field {:?} is absent", field) 105 | } 106 | FileSize(err: ParseIntError) { 107 | from() 108 | display("error parsing file size: {}", err) 109 | description("error parsing file size") 110 | } 111 | } 112 | } 113 | 114 | quick_error! { 115 | #[derive(Debug)] 116 | pub enum RepositoryError { 117 | PackageConflict(pkg: Package) { 118 | description("package we are trying to add is already in repo") 119 | display("package {}-{}-{} is already in repository", 120 | pkg.name, pkg.version, pkg.architecture) 121 | } 122 | Release(path: PathBuf, err: ReleaseFileRead) { 123 | description("can't open Release file") 124 | display("can't open {:?}: {}", path, err) 125 | context(path: AsRef, err: ReleaseFileRead) 126 | -> (path.as_ref().to_path_buf(), err) 127 | } 128 | Packages(path: PathBuf, err: PackagesRead) { 129 | description("can't open Packages file") 130 | display("can't open {:?}: {}", path, err) 131 | context(path: AsRef, err: PackagesRead) 132 | -> (path.as_ref().to_path_buf(), err) 133 | } 134 | } 135 | } 136 | 137 | impl Release { 138 | fn read(path: &Path) -> Result { 139 | use self::ReleaseFileRead::*; 140 | let mut datas = try!(parse_control(try!(File::open(path)))); 141 | if datas.len() != 1 { 142 | return Err(ExcessiveControlData); 143 | } 144 | let mut data = datas.pop().unwrap(); 145 | let codename = try!(data.remove(&"Codename".into()) 146 | .ok_or(AbsentField("Codename"))); 147 | let architectures = try!(data.remove(&"Architectures".into()) 148 | .ok_or(AbsentField("Architectures"))) 149 | .split_whitespace() 150 | .map(ToString::to_string).collect(); 151 | let components = try!(data.remove(&"Components".into()) 152 | .ok_or(AbsentField("Components"))) 153 | .split_whitespace() 154 | .map(ToString::to_string).collect(); 155 | let files = data.get(&"SHA256".into()).map(|x| &x[..]).unwrap_or("") 156 | .split("\n"); 157 | let mut hashsums = BTreeMap::new(); 158 | for line in files { 159 | let line = line.trim(); 160 | if line == "" { continue; } 161 | let mut iter = line.split_whitespace(); 162 | match (iter.next(), iter.next(), iter.next(), iter.next()) { 163 | (Some(hash), Some(size), Some(fname), None) => { 164 | let size = try!(size.parse()); 165 | hashsums.insert(fname.to_string(), 166 | (size, hash.to_string())); 167 | } 168 | _ => { 169 | return Err(InvalidHashLine); 170 | } 171 | } 172 | } 173 | Ok(Release { 174 | codename: codename, 175 | architectures: architectures, 176 | components: components, 177 | sha256: hashsums, 178 | }) 179 | } 180 | fn output(&self, out: &mut W) -> io::Result<()> { 181 | try!(out.write_kv("Codename", &self.codename)); 182 | // TODO(tailhook) better use latest date from packages 183 | // to make rebuilding the indices reproducible 184 | try!(out.write_kv("Date", &format!("{}", now_utc().rfc822z()))); 185 | try!(out.write_kv("Architectures", 186 | &self.architectures.iter().map(|x| &x[..]) 187 | .collect::>()[..].join(" "))); 188 | try!(out.write_kv("Components", 189 | &self.components.iter().map(|x| &x[..]) 190 | .collect::>()[..].join(" "))); 191 | try!(out.write_kv_lines("SHA256", 192 | self.sha256.iter().map(|(fname, &(size, ref hash))| { 193 | format!("{} {} {}", hash, size, fname) 194 | }))); 195 | Ok(()) 196 | } 197 | } 198 | 199 | impl Packages { 200 | fn read(path: &Path) -> Result { 201 | use self::PackagesRead::*; 202 | let mut coll = BTreeMap::new(); 203 | let items = try!(parse_control(try!(File::open(path)))); 204 | for mut control in items.into_iter() { 205 | let name = try!(control.remove(&"Package".into()) 206 | .ok_or(AbsentField("Package"))); 207 | let version = Version(try!(control.remove(&"Version".into()) 208 | .ok_or(AbsentField("Version")))); 209 | let architecture = try!(control.remove(&"Architecture".into()) 210 | .ok_or(AbsentField("Architecture"))); 211 | coll.entry((name.clone(), architecture.clone())) 212 | .or_insert_with(BTreeMap::new) 213 | .insert(version.clone(), Package { 214 | name: name, 215 | version: version, 216 | architecture: architecture, 217 | filename: try!(control.remove(&"Filename".into()) 218 | .ok_or(AbsentField("Filename"))).into(), 219 | size: try!(try!(control.remove(&"Size".into()) 220 | .ok_or(AbsentField("Size"))).parse()), 221 | sha256: try!(control.remove(&"SHA256".into()) 222 | .ok_or(AbsentField("SHA256"))), 223 | metadata: control.into_iter().collect(), 224 | }); 225 | } 226 | Ok(Packages(coll)) 227 | } 228 | fn output(&self, out: &mut W) -> io::Result<()> { 229 | for versions in self.0.values() { 230 | for p in versions.values() { 231 | try!(out.write_kv("Package", &p.name)); 232 | try!(out.write_kv("Version", p.version.as_ref())); 233 | try!(out.write_kv("Architecture", &p.architecture)); 234 | try!(out.write_kv("Filename", 235 | &p.filename.to_str().expect("package name should be ascii"))); 236 | try!(out.write_kv("SHA256", &p.sha256)); 237 | try!(out.write_kv("Size", &format!("{}", p.size))); 238 | for (k, v) in &p.metadata { 239 | if *k != UniCase::new("Package") && 240 | *k != UniCase::new("Version") && 241 | *k != UniCase::new("Architecture") 242 | { 243 | try!(out.write_kv(k, v)); 244 | } 245 | } 246 | try!(out.write_all(b"\n")); 247 | } 248 | } 249 | Ok(()) 250 | } 251 | pub fn new() -> Packages { 252 | Packages(BTreeMap::new()) 253 | } 254 | } 255 | 256 | 257 | impl<'a> Component<'a> { 258 | pub fn add_package(&mut self, pack: &PackageMeta, 259 | on_conflict: ConflictResolution) 260 | -> Result<(), RepositoryError> 261 | { 262 | let info = self.1.entry(pack.filename.clone()) 263 | .or_insert_with(|| { 264 | let filename = pack.filename.file_name() 265 | .expect("package path should have a filename"); 266 | let tpath = Path::new("pool") 267 | .join(pack.name.chars().take(1).collect::()) 268 | .join(&pack.name) 269 | .join(filename); 270 | 271 | // TODO(tailhook) report errors in some nicer way 272 | let hash = hash_file(&pack.filename) 273 | .expect("read file"); 274 | let size = metadata(&pack.filename) 275 | .expect("read file") 276 | .len(); 277 | 278 | FileInfo { 279 | path: tpath, 280 | sha256: hash, 281 | size: size, 282 | } 283 | }); 284 | let pkg = Package { 285 | name: pack.name.clone(), 286 | version: Version(pack.version.clone()), 287 | architecture: pack.arch.clone(), 288 | filename: info.path.clone(), 289 | sha256: info.sha256.clone(), 290 | size: info.size, 291 | metadata: pack.info.iter() 292 | .map(|(k, v)| (k.clone(), v.clone())).collect(), 293 | }; 294 | let component_arch = (pack.name.clone(), pack.arch.clone()); 295 | let versions = (self.0).0.entry(component_arch) 296 | .or_insert_with(BTreeMap::new); 297 | if versions.contains_key(&pkg.version) { 298 | use self::ConflictResolution::*; 299 | match on_conflict { 300 | Error => Err(RepositoryError::PackageConflict(pkg)), 301 | Keep => Ok(()), 302 | Replace => { 303 | versions.insert(pkg.version.clone(), pkg); 304 | Ok(()) 305 | } 306 | } 307 | } else { 308 | versions.insert(pkg.version.clone(), pkg); 309 | Ok(()) 310 | } 311 | } 312 | } 313 | 314 | impl Repository { 315 | pub fn new(base_dir: &Path) -> Repository { 316 | Repository { 317 | root: base_dir.to_path_buf(), 318 | suites: HashMap::new(), 319 | components: HashMap::new(), 320 | files: HashMap::new(), 321 | } 322 | } 323 | pub fn open(&mut self, suite: &str, component: &str, arch: &str) 324 | -> Result 325 | { 326 | if !self.suites.contains_key(suite) { 327 | let release_file = self.root.join("dists").join(suite) 328 | .join("Release"); 329 | let rel = if release_file.exists() { 330 | try!(Release::read(&release_file).context(&release_file)) 331 | } else { 332 | Release { 333 | codename: String::from(suite), 334 | architectures: BTreeSet::new(), 335 | components: BTreeSet::new(), 336 | sha256: BTreeMap::new(), 337 | } 338 | }; 339 | self.suites.insert(String::from(suite), rel); 340 | } 341 | let s = self.suites.get_mut(suite).unwrap(); 342 | s.architectures.insert(String::from(arch)); 343 | s.components.insert(String::from(component)); 344 | 345 | let triple = (String::from(suite), String::from(component), 346 | String::from(arch)); 347 | if !self.components.contains_key(&triple) { 348 | let packages_file = self.root.join("dists").join(suite) 349 | .join(component).join(format!("binary-{}/Packages", arch)); 350 | let packages = if packages_file.exists() { 351 | try!(Packages::read(&packages_file).context(&packages_file)) 352 | } else { 353 | Packages::new() 354 | }; 355 | self.components.insert(triple.clone(), packages); 356 | } 357 | let packages = self.components.get_mut(&triple).unwrap(); 358 | Ok(Component(packages, &mut self.files)) 359 | } 360 | pub fn trim(&mut self, suite: &str, cmp: &str, limit: usize) { 361 | assert!(limit > 0); 362 | for (&(ref rs, ref rcmp, _), ref mut pkgs) 363 | in self.components.iter_mut() 364 | { 365 | if rs == suite && rcmp == cmp { 366 | for (_, ref mut collection) in pkgs.0.iter_mut() { 367 | while collection.len() > limit { 368 | let smallest = collection.keys() 369 | .next().unwrap().clone(); 370 | collection.remove(&smallest); 371 | } 372 | } 373 | } 374 | } 375 | } 376 | pub fn write(mut self) -> io::Result<()> { 377 | if self.suites.len() == 0 && self.components.len() == 0 { 378 | return Ok(()); 379 | } 380 | 381 | let mut tempfiles = Vec::new(); 382 | for ((suite, cmp, arch), pkg) in self.components { 383 | let dir = self.root 384 | .join("dists").join(&suite).join(&cmp) 385 | .join(format!("binary-{}", arch)); 386 | try!(create_dir_all(&dir)); 387 | let tmp = dir.join("Packages.tmp"); 388 | let mut buf = Vec::with_capacity(16384); 389 | try!(pkg.output(&mut buf)); 390 | try!(File::create(&tmp).and_then(|mut f| f.write_all(&buf))); 391 | tempfiles.push((tmp, dir.join("Packages"))); 392 | 393 | let mut hash = Sha256::new(); 394 | hash.input(&buf); 395 | 396 | self.suites.get_mut(&suite).expect("suite already created") 397 | .sha256.insert(format!("{}/binary-{}/Packages", cmp, arch), 398 | (buf.len() as u64, format!("{:x}", hash.result()))); 399 | } 400 | for (_, suite) in self.suites { 401 | let dir = self.root.join("dists").join(&suite.codename); 402 | let tmp = dir.join("Release.tmp"); 403 | let ref mut buf = BufWriter::new(try!(File::create(&tmp))); 404 | try!(suite.output(buf)); 405 | tempfiles.push((tmp, dir.join("Release"))); 406 | } 407 | for (ref src, ref info) in self.files { 408 | let realdest = self.root.join(&info.path); 409 | let tmpname = realdest.with_file_name( 410 | String::from(realdest.file_name().unwrap().to_str().unwrap()) 411 | + ".tmp"); 412 | try!(create_dir_all(&realdest.parent().unwrap())); 413 | try!(copy(src, &tmpname)); 414 | tempfiles.push((tmpname, realdest)); 415 | } 416 | for &(ref a, ref b) in &tempfiles { 417 | try!(rename(a, b)); 418 | } 419 | Ok(()) 420 | } 421 | } 422 | -------------------------------------------------------------------------------- /src/ver/mod.rs: -------------------------------------------------------------------------------- 1 | use std::io::{self, stdout, stderr, Write, BufWriter, BufReader}; 2 | use std::fs::{File, remove_file, rename}; 3 | use std::path::{Path, PathBuf}; 4 | use std::error::Error; 5 | use std::process::{Command, exit}; 6 | use std::collections::HashMap; 7 | 8 | use argparse::{ArgumentParser, Parse, StoreTrue, List, StoreConst}; 9 | use git2::{Repository, DescribeOptions, DescribeFormatOptions}; 10 | 11 | use config::{Config}; 12 | use version::{Version}; 13 | use ver::scanner::{Scanner, Lines, Iter}; 14 | use ver::bump::Bump; 15 | 16 | mod scanner; 17 | mod commit; 18 | mod bump; 19 | 20 | 21 | #[derive(Debug, Eq, PartialEq, Ord, PartialOrd, Clone, Copy)] 22 | enum Verbosity { 23 | Quiet, 24 | Normal, 25 | Verbose, 26 | } 27 | 28 | pub fn get(cfg: &Config, dir: &Path) -> Result, Box> { 29 | for item in &cfg.versions { 30 | if item.partial_version.is_some() { // can't get from partial version 31 | continue; 32 | } 33 | let scanner = try!(scanner::Scanner::new(&item) 34 | .map_err(|e| format!("One of the regexps is wrong: {} for {:#?}", 35 | e, cfg))); 36 | for filename in item.file.iter().chain(&item.files) { 37 | let file = match File::open(&dir.join(&filename)) { 38 | Ok(i) => BufReader::new(i), 39 | Err(ref e) if e.kind() == io::ErrorKind::NotFound => continue, 40 | Err(e) => return Err(e.into()), 41 | }; 42 | let mut iter = scanner.start(); 43 | for res in Lines::iter(file) { 44 | let (lineno, line) = try!(res); 45 | match iter.line(lineno, &line) { 46 | Some((start, end)) => { 47 | return Ok(Version(line[start..end].to_string())); 48 | } 49 | None => {} 50 | } 51 | } 52 | try!(iter.error()); 53 | } 54 | } 55 | return Err("Version not found".into()); 56 | } 57 | 58 | fn _check(config: &Path, dir: &Path, git_ver: Option<(String, bool)>) 59 | -> Result> 60 | { 61 | let git_vers = git_ver.as_ref() 62 | .map(|&(ref x, exact)| (x.trim_left_matches('v'), exact)) 63 | .map(|(x, exact)| { 64 | if exact { 65 | (x, x) 66 | } else { 67 | (x, &x[..x.find("-").unwrap_or(x.len())]) 68 | } 69 | }); 70 | let cfg = try!(Config::parse_file(&config)); 71 | let mut prev: Option = None; 72 | let mut result = true; 73 | // partial versions go after full 74 | let lst = cfg.versions.iter().filter(|x| x.partial_version.is_none()) 75 | .chain(cfg.versions.iter().filter(|x| x.partial_version.is_some())); 76 | for item in lst { 77 | let scanner = try!(scanner::Scanner::new(&item) 78 | .map_err(|e| format!("One of the regexps is wrong: {} for {:#?}", 79 | e, cfg))); 80 | for filename in item.file.iter().chain(&item.files) { 81 | let file = match File::open(&dir.join(&filename)) { 82 | Ok(i) => BufReader::new(i), 83 | Err(ref e) if e.kind() == io::ErrorKind::NotFound => continue, 84 | Err(e) => return Err(e.into()), 85 | }; 86 | let mut iter = scanner.start(); 87 | for res in Lines::iter(file) { 88 | let (lineno, line) = try!(res); 89 | match iter.line(lineno, &line) { 90 | Some((start, end)) => { 91 | let ver = &line[start..end]; 92 | println!("{}:{}: (v{}) {}", 93 | filename.display(), lineno, ver, 94 | line.trim_right()); 95 | if let Some(ref pver) = prev { 96 | let cver = scanner.partial.as_ref() 97 | .map(|re| re.captures(pver) 98 | .expect("partial-version must match") 99 | .get(0).unwrap().as_str()) 100 | .unwrap_or(&pver[..]); 101 | if cver != ver { 102 | result = false; 103 | writeln!(&mut stderr(), 104 | "{}:{}: version conflict {} != {}", 105 | filename.display(), lineno, 106 | ver, cver).ok(); 107 | } 108 | } else { 109 | if let Some((long, short)) = git_vers { 110 | if long != ver && short != ver { 111 | result = false; 112 | writeln!(&mut stderr(), 113 | "{}:{}: version conflict {} != {}", 114 | filename.display(), lineno, 115 | ver, short).ok(); 116 | prev = Some(short.to_string()); 117 | } else { 118 | prev = Some(ver.to_string()); 119 | } 120 | } else { 121 | prev = Some(ver.to_string()); 122 | } 123 | } 124 | } 125 | None => {} 126 | } 127 | } 128 | try!(iter.error()); 129 | } 130 | } 131 | if prev.is_none() { 132 | Err(format!("No version found").into()) 133 | } else { 134 | Ok(result) 135 | } 136 | } 137 | 138 | fn _set(cfg: &Config, dir: &Path, version: &str, dry_run: bool, force: bool, 139 | verbosity: Verbosity) 140 | -> Result> 141 | { 142 | let mut buf = Vec::new(); 143 | let mut result = _write_tmp(cfg, dir, version, &mut buf, force, 144 | verbosity); 145 | let mut iter = buf.into_iter(); 146 | if !dry_run && result.is_ok() { 147 | for (tmp, dest) in iter.by_ref() { 148 | match rename(&tmp, &dest) { 149 | Ok(()) => {} 150 | Err(e) => { 151 | result = Err(format!( 152 | "Error renaming file {:?}: {}", tmp, e).into()); 153 | remove_file(&tmp) 154 | .or_else(|e| writeln!(&mut stderr(), 155 | "Error removing file {:?}: {}", tmp, e)).ok(); 156 | } 157 | } 158 | } 159 | } 160 | for (tmp, _) in iter { 161 | remove_file(&tmp) 162 | .or_else(|e| writeln!(&mut stderr(), 163 | "Error removing file {:?}: {}", tmp, e)).ok(); 164 | } 165 | return result; 166 | } 167 | 168 | fn _write_tmp(cfg: &Config, dir: &Path, version: &str, 169 | files: &mut Vec<(PathBuf, PathBuf)>, force: bool, verbosity: Verbosity) 170 | -> Result> 171 | { 172 | let mut prev: Option = None; 173 | let mut result = Ok(()); 174 | let mut scanners = HashMap::new(); 175 | for item in &cfg.versions { 176 | let scanner = try!(scanner::Scanner::new(&item) 177 | .map_err(|e| format!("One of the regexps is wrong: {} for {:#?}", 178 | e, cfg))); 179 | for file in item.file.iter().chain(&item.files) { 180 | scanners.entry(file.clone()) 181 | .or_insert_with(Vec::new) 182 | .push(scanner.clone()) 183 | } 184 | } 185 | for (filename, list) in scanners { 186 | let filename = dir.join(filename); 187 | let mut tmp = filename.as_os_str().to_owned(); 188 | tmp.push(".tmp"); 189 | let tmp = tmp.into(); 190 | let file = match File::open(&filename) { 191 | Ok(i) => BufReader::new(i), 192 | Err(ref e) if e.kind() == io::ErrorKind::NotFound => continue, 193 | Err(e) => return Err(e.into()), 194 | }; 195 | 196 | let mut out = BufWriter::new(try!(File::create(&tmp))); 197 | files.push((tmp, filename.to_path_buf())); 198 | 199 | let mut scanners = list.iter().map(Scanner::start).collect::>(); 200 | try!(Lines::iter(file).map(|res| { 201 | let (lineno, line) = try!(res); 202 | let nline = scanners.iter_mut().fold(line, |line, citer| { 203 | match citer.line(lineno, &line) { 204 | Some((start, end)) => { 205 | let ver = &line[start..end]; 206 | 207 | let partver = citer.scanner().partial.as_ref() 208 | .map(|re| re.captures(version) 209 | .expect("partial-version must match") 210 | .get(0).unwrap().as_str()) 211 | .unwrap_or(&version[..]); 212 | 213 | let nline = String::from(&line[..start]) 214 | + partver + &line[end..]; 215 | 216 | if verbosity == Verbosity::Verbose { 217 | writeln!(&mut stderr(), "{}:{}: (v{} -> v{}) {}", 218 | filename.display(), lineno, ver, partver, 219 | nline.trim_right()).ok(); 220 | } 221 | if let Some(ref pver) = prev { 222 | let cver = citer.scanner().partial.as_ref() 223 | .map(|re| re.captures(pver) 224 | .expect("partial-version must match") 225 | .get(0).unwrap().as_str()) 226 | .unwrap_or(&pver[..]); 227 | if cver != ver { 228 | let msg = format!( 229 | "{}:{}: version conflict {} != {}", 230 | filename.display(), lineno, 231 | ver, cver); 232 | if force { 233 | writeln!(&mut stderr(), "{}", msg).ok(); 234 | } else { 235 | result = Err(msg.into()); 236 | } 237 | } 238 | } else { 239 | if citer.scanner().partial.is_none() { 240 | // TODO(tailhook) we skip checking partial 241 | // version is it's not the first one 242 | // We may fix it, but probably it's not a big 243 | // deal 244 | prev = Some(ver.to_string()); 245 | } 246 | } 247 | nline 248 | } 249 | None => line, 250 | } 251 | }); 252 | out.write_all(nline.as_bytes()) 253 | }).collect::, _>>()); 254 | try!(scanners.into_iter().map(Iter::error) 255 | .collect::, _>>()); 256 | } 257 | if let Some(ver) = prev { 258 | if let Err(e) = result { 259 | Err(e) 260 | } else { 261 | if verbosity == Verbosity::Normal { 262 | writeln!(&mut stderr(), "{} -> {}", ver, version).ok(); 263 | } 264 | Ok(ver) 265 | } 266 | } else { 267 | Err(format!("No version found").into()) 268 | } 269 | } 270 | 271 | pub fn get_version(args: Vec) { 272 | let mut config = PathBuf::from("bulk.yaml"); 273 | let mut dir = PathBuf::from("."); 274 | { 275 | let mut ap = ArgumentParser::new(); 276 | ap.refer(&mut config) 277 | .add_option(&["-c", "--config"], Parse, 278 | "Package configuration file"); 279 | ap.refer(&mut dir) 280 | .add_option(&["--base-dir"], Parse, " 281 | Base directory for all paths in config. \ 282 | Current working directory by default."); 283 | match ap.parse(args, &mut stdout(), &mut stderr()) { 284 | Ok(()) => {} 285 | Err(x) => exit(x), 286 | } 287 | } 288 | 289 | let cfg = match Config::parse_file(&config) { 290 | Ok(cfg) => cfg, 291 | Err(e) => { 292 | writeln!(&mut stderr(), "Error parsing config: {}", e).ok(); 293 | exit(7); 294 | } 295 | }; 296 | 297 | match get(&cfg, &dir) { 298 | Ok(ver) => { 299 | println!("{}", ver); 300 | } 301 | Err(text) => { 302 | writeln!(&mut stderr(), "Error: {}", text).ok(); 303 | exit(1); 304 | } 305 | } 306 | } 307 | 308 | pub fn set_version(args: Vec) { 309 | let mut config = PathBuf::from("bulk.yaml"); 310 | let mut dir = PathBuf::from("."); 311 | let mut version = Version(String::new()); 312 | let mut dry_run = false; 313 | let mut force = false; 314 | let mut git = false; 315 | { 316 | let mut ap = ArgumentParser::new(); 317 | ap.refer(&mut config) 318 | .add_option(&["-c", "--config"], Parse, 319 | "Package configuration file"); 320 | ap.refer(&mut dir) 321 | .add_option(&["--base-dir"], Parse, " 322 | Base directory for all paths in config. \ 323 | Current working directory by default."); 324 | ap.refer(&mut git) 325 | .add_option(&["-g", "--git-commit-and-tag"], StoreTrue, " 326 | Commit using git and create a tag"); 327 | ap.refer(&mut dry_run) 328 | .add_option(&["--dry-run"], StoreTrue, " 329 | Don't write version, just show changes"); 330 | ap.refer(&mut force) 331 | .add_option(&["--force"], StoreTrue, " 332 | Write version even if previous values are inconsistent"); 333 | ap.refer(&mut version) 334 | .add_argument("version", Parse, "Target version") 335 | .required(); 336 | 337 | match ap.parse(args, &mut stdout(), &mut stderr()) { 338 | Ok(()) => {} 339 | Err(x) => exit(x), 340 | } 341 | } 342 | 343 | let cfg = match Config::parse_file(&config) { 344 | Ok(cfg) => cfg, 345 | Err(e) => { 346 | writeln!(&mut stderr(), "Error parsing config: {}", e).ok(); 347 | exit(7); 348 | } 349 | }; 350 | 351 | let mut git_repo = if git { 352 | match commit::check_status(&cfg, &dir) { 353 | Ok(repo) => Some(repo), 354 | Err(text) => { 355 | writeln!(&mut stderr(), "Git error: {}", text).ok(); 356 | exit(2); 357 | } 358 | } 359 | } else { 360 | None 361 | }; 362 | 363 | match _set(&cfg, &dir, version.num(), dry_run, force, 364 | Verbosity::Verbose) 365 | { 366 | Ok(_) => {} 367 | Err(text) => { 368 | writeln!(&mut stderr(), "Error: {}", text).ok(); 369 | exit(1); 370 | } 371 | } 372 | 373 | if let Some(ref mut repo) = git_repo { 374 | match commit::commit_version(&cfg, &dir, repo, &version, None, dry_run) 375 | { 376 | Ok(_) => {} 377 | Err(text) => { 378 | writeln!(&mut stderr(), "Error commiting: {}", text).ok(); 379 | exit(1); 380 | } 381 | } 382 | } 383 | } 384 | 385 | pub fn incr_version(args: Vec) { 386 | let mut config = PathBuf::from("bulk.yaml"); 387 | let mut dir = PathBuf::from("."); 388 | let mut bump = Bump::Patch; 389 | let mut dry_run = false; 390 | let mut git = false; 391 | { 392 | let mut ap = ArgumentParser::new(); 393 | ap.refer(&mut config) 394 | .add_option(&["-c", "--config"], Parse, 395 | "Package configuration file"); 396 | ap.refer(&mut dir) 397 | .add_option(&["--base-dir"], Parse, " 398 | Base directory for all paths in config. \ 399 | Current working directory by default."); 400 | ap.refer(&mut git) 401 | .add_option(&["-g", "--git-commit-and-tag"], StoreTrue, " 402 | Commit using git and create a tag"); 403 | ap.refer(&mut dry_run) 404 | .add_option(&["--dry-run"], StoreTrue, " 405 | Don't write version, just show changes"); 406 | ap.refer(&mut bump) 407 | .add_option(&["--breaking", "-b"], StoreConst(Bump::Major), " 408 | Bump semver breaking (major) version") 409 | .add_option(&["--feature", "-f"], StoreConst(Bump::Minor), " 410 | Bump semver feature (minor) version") 411 | .add_option(&["--bugfix", "-x"], StoreConst(Bump::Patch), " 412 | Bump semver bugfix (patch) version") 413 | .add_option(&["-1"], StoreConst(Bump::Component(1)), " 414 | Bump first component of a version") 415 | .add_option(&["-2"], StoreConst(Bump::Component(2)), " 416 | Bump second component of a version") 417 | .add_option(&["-3"], StoreConst(Bump::Component(3)), " 418 | Bump third component of a version") 419 | .add_option(&["-d", "--date-major"], StoreConst(Bump::DateMajor), " 420 | Set version to a date-based `yymmdd.patch`, like v180113.0. 421 | Bump patch revision if current version has the same date. 422 | Note: date is UTC one to avoid issues with time going back.") 423 | .required(); 424 | 425 | match ap.parse(args, &mut stdout(), &mut stderr()) { 426 | Ok(()) => {} 427 | Err(x) => exit(x), 428 | } 429 | } 430 | 431 | let cfg = match Config::parse_file(&config) { 432 | Ok(cfg) => cfg, 433 | Err(e) => { 434 | writeln!(&mut stderr(), "Error parsing config: {}", e).ok(); 435 | exit(7); 436 | } 437 | }; 438 | 439 | let mut git_repo = if git { 440 | match commit::check_status(&cfg, &dir) { 441 | Ok(repo) => Some(repo), 442 | Err(text) => { 443 | writeln!(&mut stderr(), "Git error: {}", text).ok(); 444 | exit(2); 445 | } 446 | } 447 | } else { 448 | None 449 | }; 450 | 451 | let ver = match get(&cfg, &dir) { 452 | Ok(ver) => ver, 453 | Err(text) => { 454 | writeln!(&mut stderr(), "Error: {}", text).ok(); 455 | exit(1); 456 | } 457 | }; 458 | let nver = match bump::bump_version(&ver, bump) { 459 | Ok(x) => x, 460 | Err(e) => { 461 | writeln!(&mut stderr(), "Can't bump version {:?}: {}", ver, e) 462 | .ok(); 463 | exit(1); 464 | } 465 | }; 466 | println!("Bumping {} -> {}", ver, nver); 467 | 468 | match _set(&cfg, &dir, nver.num(), dry_run, false, 469 | Verbosity::Verbose) 470 | { 471 | Ok(_) => {} 472 | Err(text) => { 473 | writeln!(&mut stderr(), "Error: {}", text).ok(); 474 | exit(1); 475 | } 476 | } 477 | 478 | if let Some(ref mut repo) = git_repo { 479 | match commit::commit_version(&cfg, &dir, repo, &nver, 480 | Some(&ver), dry_run) 481 | { 482 | Ok(_) => {} 483 | Err(text) => { 484 | writeln!(&mut stderr(), "Error commiting: {}", text).ok(); 485 | exit(1); 486 | } 487 | } 488 | } 489 | } 490 | 491 | pub fn git_version() -> Result> { 492 | let repo = Repository::open(".")?; 493 | let descr = repo.describe(&DescribeOptions::default())?; 494 | Ok(descr.format(Some( 495 | DescribeFormatOptions::new() 496 | .dirty_suffix("-dirty") 497 | ))?) 498 | } 499 | 500 | pub fn check_version(args: Vec) { 501 | let mut config = PathBuf::from("bulk.yaml"); 502 | let mut dir = PathBuf::from("."); 503 | let mut git_describe = false; 504 | let mut git_exact = false; 505 | { 506 | let mut ap = ArgumentParser::new(); 507 | ap.refer(&mut config) 508 | .add_option(&["-c", "--config"], Parse, 509 | "Package configuration file"); 510 | ap.refer(&mut git_describe) 511 | .add_option(&["-g", "--git-describe"], StoreTrue, 512 | "Check that version matches `git describe`"); 513 | ap.refer(&mut git_exact) 514 | .add_option(&["-G", "--git-describe-exact"], StoreTrue, 515 | "Check that `git describe` matches version exactly \ 516 | (current commit is a tag commit"); 517 | ap.refer(&mut dir) 518 | .add_option(&["--base-dir"], Parse, " 519 | Base directory for all paths in config. \ 520 | Current working directory by default."); 521 | match ap.parse(args, &mut stdout(), &mut stderr()) { 522 | Ok(()) => {} 523 | Err(x) => exit(x), 524 | } 525 | } 526 | 527 | let version = if git_describe || git_exact { 528 | match git_version() { 529 | Ok(ver) => Some(ver), 530 | Err(e) => { 531 | writeln!(&mut stderr(), "Git error: {}", e).ok(); 532 | exit(2); 533 | } 534 | } 535 | } else { 536 | None 537 | }; 538 | 539 | match _check(&config, &dir, version.map(|v| (v, git_exact))) { 540 | Ok(val) => { 541 | exit(if val { 0 } else { 1 }); 542 | } 543 | Err(text) => { 544 | writeln!(&mut stderr(), "Error: {}", text).ok(); 545 | exit(1); 546 | } 547 | } 548 | } 549 | 550 | pub fn with_version(args: Vec) { 551 | let mut config = PathBuf::from("bulk.yaml"); 552 | let mut dir = PathBuf::from("."); 553 | let mut version = Version(String::new()); 554 | let mut cmdline = Vec::::new(); 555 | let mut verbosity = Verbosity::Normal; 556 | { 557 | let mut ap = ArgumentParser::new(); 558 | ap.refer(&mut config) 559 | .add_option(&["-c", "--config"], Parse, 560 | "Package configuration file"); 561 | ap.refer(&mut dir) 562 | .add_option(&["--base-dir"], Parse, " 563 | Base directory for all paths in config. \ 564 | Current working directory by default."); 565 | ap.refer(&mut verbosity) 566 | .add_option(&["--quiet"], StoreConst(Verbosity::Quiet), " 567 | Don't print anything") 568 | .add_option(&["--verbose"], StoreConst(Verbosity::Verbose), " 569 | Print file lines an versions changed. By default we just print 570 | old an the new versions."); 571 | ap.refer(&mut version) 572 | .add_argument("version", Parse, "Target version") 573 | .required(); 574 | ap.refer(&mut cmdline) 575 | .add_argument("cmd", List, "Command and arguments") 576 | .required(); 577 | ap.stop_on_first_argument(true); 578 | 579 | match ap.parse(args, &mut stdout(), &mut stderr()) { 580 | Ok(()) => {} 581 | Err(x) => exit(x), 582 | } 583 | } 584 | _with_version(&config, &dir, version, cmdline, verbosity) 585 | } 586 | 587 | pub fn with_git_version(args: Vec) { 588 | let mut config = PathBuf::from("bulk.yaml"); 589 | let mut dir = PathBuf::from("."); 590 | let mut cmdline = Vec::::new(); 591 | let mut verbosity = Verbosity::Normal; 592 | { 593 | let mut ap = ArgumentParser::new(); 594 | ap.refer(&mut config) 595 | .add_option(&["-c", "--config"], Parse, 596 | "Package configuration file"); 597 | ap.refer(&mut dir) 598 | .add_option(&["--base-dir"], Parse, " 599 | Base directory for all paths in config. \ 600 | Current working directory by default."); 601 | ap.refer(&mut verbosity) 602 | .add_option(&["--quiet"], StoreConst(Verbosity::Quiet), " 603 | Don't print anything") 604 | .add_option(&["--verbose"], StoreConst(Verbosity::Verbose), " 605 | Print file lines an versions changed. By default we just print 606 | old an the new versions."); 607 | ap.refer(&mut cmdline) 608 | .add_argument("cmd", List, "Command and arguments") 609 | .required(); 610 | ap.stop_on_first_argument(true); 611 | 612 | match ap.parse(args, &mut stdout(), &mut stderr()) { 613 | Ok(()) => {} 614 | Err(x) => exit(x), 615 | } 616 | } 617 | let version = match git_version() { 618 | Ok(ver) => Version(ver), 619 | Err(e) => { 620 | writeln!(&mut stderr(), "Git error: {}", e).ok(); 621 | exit(2); 622 | } 623 | }; 624 | _with_version(&config, &dir, version, cmdline, verbosity) 625 | } 626 | 627 | fn _with_version(config: &Path, dir: &Path, version: Version, 628 | mut cmdline: Vec, verbosity: Verbosity) 629 | { 630 | let cfg = match Config::parse_file(&config) { 631 | Ok(cfg) => cfg, 632 | Err(e) => { 633 | writeln!(&mut stderr(), "Error parsing config: {}", e).ok(); 634 | exit(7); 635 | } 636 | }; 637 | 638 | let old = match _set(&cfg, dir, version.num(), false, false, verbosity) 639 | { 640 | Ok(ver) => ver, 641 | Err(text) => { 642 | writeln!(&mut stderr(), "Error: {}", text).ok(); 643 | exit(99); 644 | } 645 | }; 646 | 647 | let mut cmd = Command::new(cmdline.remove(0)); 648 | cmd.args(&cmdline); 649 | let result = cmd.status(); 650 | 651 | match _set(&cfg, &dir, &old, false, false, verbosity) { 652 | Ok(_) => {} 653 | Err(text) => { 654 | writeln!(&mut stderr(), "Error: {}", text).ok(); 655 | exit(99); 656 | } 657 | } 658 | 659 | match result { 660 | Ok(s) => { 661 | if let Some(x) = s.code() { 662 | exit(x); 663 | } else { 664 | exit(98); 665 | } 666 | } 667 | Err(e) => { 668 | writeln!(&mut stderr(), "Error running command: {}", e).ok(); 669 | exit(98); 670 | } 671 | } 672 | } 673 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | [[package]] 2 | name = "adler32" 3 | version = "1.0.3" 4 | source = "registry+https://github.com/rust-lang/crates.io-index" 5 | 6 | [[package]] 7 | name = "aho-corasick" 8 | version = "0.6.5" 9 | source = "registry+https://github.com/rust-lang/crates.io-index" 10 | dependencies = [ 11 | "memchr 2.0.1 (registry+https://github.com/rust-lang/crates.io-index)", 12 | ] 13 | 14 | [[package]] 15 | name = "argparse" 16 | version = "0.2.1" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | 19 | [[package]] 20 | name = "arrayref" 21 | version = "0.3.4" 22 | source = "registry+https://github.com/rust-lang/crates.io-index" 23 | 24 | [[package]] 25 | name = "assert_cli" 26 | version = "0.6.2" 27 | source = "registry+https://github.com/rust-lang/crates.io-index" 28 | dependencies = [ 29 | "colored 1.6.1 (registry+https://github.com/rust-lang/crates.io-index)", 30 | "difference 2.0.0 (registry+https://github.com/rust-lang/crates.io-index)", 31 | "environment 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", 32 | "failure 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", 33 | "failure_derive 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", 34 | "serde_json 1.0.22 (registry+https://github.com/rust-lang/crates.io-index)", 35 | ] 36 | 37 | [[package]] 38 | name = "atty" 39 | version = "0.2.10" 40 | source = "registry+https://github.com/rust-lang/crates.io-index" 41 | dependencies = [ 42 | "libc 0.2.42 (registry+https://github.com/rust-lang/crates.io-index)", 43 | "termion 1.5.1 (registry+https://github.com/rust-lang/crates.io-index)", 44 | "winapi 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)", 45 | ] 46 | 47 | [[package]] 48 | name = "backtrace" 49 | version = "0.3.9" 50 | source = "registry+https://github.com/rust-lang/crates.io-index" 51 | dependencies = [ 52 | "backtrace-sys 0.1.23 (registry+https://github.com/rust-lang/crates.io-index)", 53 | "cfg-if 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)", 54 | "libc 0.2.42 (registry+https://github.com/rust-lang/crates.io-index)", 55 | "rustc-demangle 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)", 56 | "winapi 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)", 57 | ] 58 | 59 | [[package]] 60 | name = "backtrace-sys" 61 | version = "0.1.23" 62 | source = "registry+https://github.com/rust-lang/crates.io-index" 63 | dependencies = [ 64 | "cc 1.0.17 (registry+https://github.com/rust-lang/crates.io-index)", 65 | "libc 0.2.42 (registry+https://github.com/rust-lang/crates.io-index)", 66 | ] 67 | 68 | [[package]] 69 | name = "bitflags" 70 | version = "1.0.3" 71 | source = "registry+https://github.com/rust-lang/crates.io-index" 72 | 73 | [[package]] 74 | name = "block-buffer" 75 | version = "0.3.3" 76 | source = "registry+https://github.com/rust-lang/crates.io-index" 77 | dependencies = [ 78 | "arrayref 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)", 79 | "byte-tools 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", 80 | ] 81 | 82 | [[package]] 83 | name = "build_const" 84 | version = "0.2.1" 85 | source = "registry+https://github.com/rust-lang/crates.io-index" 86 | 87 | [[package]] 88 | name = "bulk" 89 | version = "0.4.12" 90 | dependencies = [ 91 | "argparse 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", 92 | "assert_cli 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)", 93 | "env_logger 0.5.10 (registry+https://github.com/rust-lang/crates.io-index)", 94 | "failure 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", 95 | "git2 0.7.2 (registry+https://github.com/rust-lang/crates.io-index)", 96 | "lazy_static 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)", 97 | "libflate 0.1.16 (registry+https://github.com/rust-lang/crates.io-index)", 98 | "log 0.4.3 (registry+https://github.com/rust-lang/crates.io-index)", 99 | "matches 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)", 100 | "quick-error 1.2.2 (registry+https://github.com/rust-lang/crates.io-index)", 101 | "quire 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", 102 | "regex 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)", 103 | "scan_dir 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)", 104 | "serde 1.0.70 (registry+https://github.com/rust-lang/crates.io-index)", 105 | "serde_derive 1.0.70 (registry+https://github.com/rust-lang/crates.io-index)", 106 | "sha2 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)", 107 | "tar 0.4.16 (registry+https://github.com/rust-lang/crates.io-index)", 108 | "tempfile 3.0.2 (registry+https://github.com/rust-lang/crates.io-index)", 109 | "time 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)", 110 | "unicase 2.1.0 (registry+https://github.com/rust-lang/crates.io-index)", 111 | ] 112 | 113 | [[package]] 114 | name = "byte-tools" 115 | version = "0.2.0" 116 | source = "registry+https://github.com/rust-lang/crates.io-index" 117 | 118 | [[package]] 119 | name = "byteorder" 120 | version = "1.2.3" 121 | source = "registry+https://github.com/rust-lang/crates.io-index" 122 | 123 | [[package]] 124 | name = "cc" 125 | version = "1.0.17" 126 | source = "registry+https://github.com/rust-lang/crates.io-index" 127 | 128 | [[package]] 129 | name = "cfg-if" 130 | version = "0.1.4" 131 | source = "registry+https://github.com/rust-lang/crates.io-index" 132 | 133 | [[package]] 134 | name = "cmake" 135 | version = "0.1.31" 136 | source = "registry+https://github.com/rust-lang/crates.io-index" 137 | dependencies = [ 138 | "cc 1.0.17 (registry+https://github.com/rust-lang/crates.io-index)", 139 | ] 140 | 141 | [[package]] 142 | name = "colored" 143 | version = "1.6.1" 144 | source = "registry+https://github.com/rust-lang/crates.io-index" 145 | dependencies = [ 146 | "lazy_static 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)", 147 | ] 148 | 149 | [[package]] 150 | name = "crc" 151 | version = "1.8.1" 152 | source = "registry+https://github.com/rust-lang/crates.io-index" 153 | dependencies = [ 154 | "build_const 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", 155 | ] 156 | 157 | [[package]] 158 | name = "difference" 159 | version = "2.0.0" 160 | source = "registry+https://github.com/rust-lang/crates.io-index" 161 | 162 | [[package]] 163 | name = "digest" 164 | version = "0.7.4" 165 | source = "registry+https://github.com/rust-lang/crates.io-index" 166 | dependencies = [ 167 | "generic-array 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)", 168 | ] 169 | 170 | [[package]] 171 | name = "dtoa" 172 | version = "0.4.3" 173 | source = "registry+https://github.com/rust-lang/crates.io-index" 174 | 175 | [[package]] 176 | name = "env_logger" 177 | version = "0.5.10" 178 | source = "registry+https://github.com/rust-lang/crates.io-index" 179 | dependencies = [ 180 | "atty 0.2.10 (registry+https://github.com/rust-lang/crates.io-index)", 181 | "humantime 1.1.1 (registry+https://github.com/rust-lang/crates.io-index)", 182 | "log 0.4.3 (registry+https://github.com/rust-lang/crates.io-index)", 183 | "regex 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)", 184 | "termcolor 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", 185 | ] 186 | 187 | [[package]] 188 | name = "environment" 189 | version = "0.1.1" 190 | source = "registry+https://github.com/rust-lang/crates.io-index" 191 | 192 | [[package]] 193 | name = "failure" 194 | version = "0.1.1" 195 | source = "registry+https://github.com/rust-lang/crates.io-index" 196 | dependencies = [ 197 | "backtrace 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)", 198 | "failure_derive 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", 199 | ] 200 | 201 | [[package]] 202 | name = "failure_derive" 203 | version = "0.1.1" 204 | source = "registry+https://github.com/rust-lang/crates.io-index" 205 | dependencies = [ 206 | "quote 0.3.15 (registry+https://github.com/rust-lang/crates.io-index)", 207 | "syn 0.11.11 (registry+https://github.com/rust-lang/crates.io-index)", 208 | "synstructure 0.6.1 (registry+https://github.com/rust-lang/crates.io-index)", 209 | ] 210 | 211 | [[package]] 212 | name = "fake-simd" 213 | version = "0.1.2" 214 | source = "registry+https://github.com/rust-lang/crates.io-index" 215 | 216 | [[package]] 217 | name = "filetime" 218 | version = "0.2.1" 219 | source = "registry+https://github.com/rust-lang/crates.io-index" 220 | dependencies = [ 221 | "cfg-if 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)", 222 | "libc 0.2.42 (registry+https://github.com/rust-lang/crates.io-index)", 223 | "redox_syscall 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)", 224 | ] 225 | 226 | [[package]] 227 | name = "fuchsia-zircon" 228 | version = "0.3.3" 229 | source = "registry+https://github.com/rust-lang/crates.io-index" 230 | dependencies = [ 231 | "bitflags 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)", 232 | "fuchsia-zircon-sys 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)", 233 | ] 234 | 235 | [[package]] 236 | name = "fuchsia-zircon-sys" 237 | version = "0.3.3" 238 | source = "registry+https://github.com/rust-lang/crates.io-index" 239 | 240 | [[package]] 241 | name = "generic-array" 242 | version = "0.9.0" 243 | source = "registry+https://github.com/rust-lang/crates.io-index" 244 | dependencies = [ 245 | "typenum 1.10.0 (registry+https://github.com/rust-lang/crates.io-index)", 246 | ] 247 | 248 | [[package]] 249 | name = "git2" 250 | version = "0.7.2" 251 | source = "registry+https://github.com/rust-lang/crates.io-index" 252 | dependencies = [ 253 | "bitflags 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)", 254 | "libc 0.2.42 (registry+https://github.com/rust-lang/crates.io-index)", 255 | "libgit2-sys 0.7.4 (registry+https://github.com/rust-lang/crates.io-index)", 256 | "log 0.4.3 (registry+https://github.com/rust-lang/crates.io-index)", 257 | "url 1.7.1 (registry+https://github.com/rust-lang/crates.io-index)", 258 | ] 259 | 260 | [[package]] 261 | name = "humannum" 262 | version = "0.1.0" 263 | source = "registry+https://github.com/rust-lang/crates.io-index" 264 | dependencies = [ 265 | "num-traits 0.1.43 (registry+https://github.com/rust-lang/crates.io-index)", 266 | "quick-error 1.2.2 (registry+https://github.com/rust-lang/crates.io-index)", 267 | ] 268 | 269 | [[package]] 270 | name = "humantime" 271 | version = "1.1.1" 272 | source = "registry+https://github.com/rust-lang/crates.io-index" 273 | dependencies = [ 274 | "quick-error 1.2.2 (registry+https://github.com/rust-lang/crates.io-index)", 275 | ] 276 | 277 | [[package]] 278 | name = "idna" 279 | version = "0.1.5" 280 | source = "registry+https://github.com/rust-lang/crates.io-index" 281 | dependencies = [ 282 | "matches 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)", 283 | "unicode-bidi 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)", 284 | "unicode-normalization 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)", 285 | ] 286 | 287 | [[package]] 288 | name = "itoa" 289 | version = "0.4.2" 290 | source = "registry+https://github.com/rust-lang/crates.io-index" 291 | 292 | [[package]] 293 | name = "lazy_static" 294 | version = "1.0.1" 295 | source = "registry+https://github.com/rust-lang/crates.io-index" 296 | 297 | [[package]] 298 | name = "libc" 299 | version = "0.2.42" 300 | source = "registry+https://github.com/rust-lang/crates.io-index" 301 | 302 | [[package]] 303 | name = "libflate" 304 | version = "0.1.16" 305 | source = "registry+https://github.com/rust-lang/crates.io-index" 306 | dependencies = [ 307 | "adler32 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)", 308 | "byteorder 1.2.3 (registry+https://github.com/rust-lang/crates.io-index)", 309 | "crc 1.8.1 (registry+https://github.com/rust-lang/crates.io-index)", 310 | ] 311 | 312 | [[package]] 313 | name = "libgit2-sys" 314 | version = "0.7.4" 315 | source = "registry+https://github.com/rust-lang/crates.io-index" 316 | dependencies = [ 317 | "cc 1.0.17 (registry+https://github.com/rust-lang/crates.io-index)", 318 | "cmake 0.1.31 (registry+https://github.com/rust-lang/crates.io-index)", 319 | "libc 0.2.42 (registry+https://github.com/rust-lang/crates.io-index)", 320 | "libz-sys 1.0.18 (registry+https://github.com/rust-lang/crates.io-index)", 321 | "pkg-config 0.3.11 (registry+https://github.com/rust-lang/crates.io-index)", 322 | ] 323 | 324 | [[package]] 325 | name = "libz-sys" 326 | version = "1.0.18" 327 | source = "registry+https://github.com/rust-lang/crates.io-index" 328 | dependencies = [ 329 | "cc 1.0.17 (registry+https://github.com/rust-lang/crates.io-index)", 330 | "libc 0.2.42 (registry+https://github.com/rust-lang/crates.io-index)", 331 | "pkg-config 0.3.11 (registry+https://github.com/rust-lang/crates.io-index)", 332 | "vcpkg 0.2.4 (registry+https://github.com/rust-lang/crates.io-index)", 333 | ] 334 | 335 | [[package]] 336 | name = "log" 337 | version = "0.4.3" 338 | source = "registry+https://github.com/rust-lang/crates.io-index" 339 | dependencies = [ 340 | "cfg-if 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)", 341 | ] 342 | 343 | [[package]] 344 | name = "matches" 345 | version = "0.1.6" 346 | source = "registry+https://github.com/rust-lang/crates.io-index" 347 | 348 | [[package]] 349 | name = "memchr" 350 | version = "2.0.1" 351 | source = "registry+https://github.com/rust-lang/crates.io-index" 352 | dependencies = [ 353 | "libc 0.2.42 (registry+https://github.com/rust-lang/crates.io-index)", 354 | ] 355 | 356 | [[package]] 357 | name = "num-traits" 358 | version = "0.1.43" 359 | source = "registry+https://github.com/rust-lang/crates.io-index" 360 | dependencies = [ 361 | "num-traits 0.2.5 (registry+https://github.com/rust-lang/crates.io-index)", 362 | ] 363 | 364 | [[package]] 365 | name = "num-traits" 366 | version = "0.2.5" 367 | source = "registry+https://github.com/rust-lang/crates.io-index" 368 | 369 | [[package]] 370 | name = "percent-encoding" 371 | version = "1.0.1" 372 | source = "registry+https://github.com/rust-lang/crates.io-index" 373 | 374 | [[package]] 375 | name = "pkg-config" 376 | version = "0.3.11" 377 | source = "registry+https://github.com/rust-lang/crates.io-index" 378 | 379 | [[package]] 380 | name = "proc-macro2" 381 | version = "0.4.6" 382 | source = "registry+https://github.com/rust-lang/crates.io-index" 383 | dependencies = [ 384 | "unicode-xid 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", 385 | ] 386 | 387 | [[package]] 388 | name = "quick-error" 389 | version = "1.2.2" 390 | source = "registry+https://github.com/rust-lang/crates.io-index" 391 | 392 | [[package]] 393 | name = "quire" 394 | version = "0.4.0" 395 | source = "registry+https://github.com/rust-lang/crates.io-index" 396 | dependencies = [ 397 | "humannum 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", 398 | "num-traits 0.1.43 (registry+https://github.com/rust-lang/crates.io-index)", 399 | "quick-error 1.2.2 (registry+https://github.com/rust-lang/crates.io-index)", 400 | "serde 1.0.70 (registry+https://github.com/rust-lang/crates.io-index)", 401 | ] 402 | 403 | [[package]] 404 | name = "quote" 405 | version = "0.3.15" 406 | source = "registry+https://github.com/rust-lang/crates.io-index" 407 | 408 | [[package]] 409 | name = "quote" 410 | version = "0.6.3" 411 | source = "registry+https://github.com/rust-lang/crates.io-index" 412 | dependencies = [ 413 | "proc-macro2 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)", 414 | ] 415 | 416 | [[package]] 417 | name = "rand" 418 | version = "0.4.2" 419 | source = "registry+https://github.com/rust-lang/crates.io-index" 420 | dependencies = [ 421 | "fuchsia-zircon 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)", 422 | "libc 0.2.42 (registry+https://github.com/rust-lang/crates.io-index)", 423 | "winapi 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)", 424 | ] 425 | 426 | [[package]] 427 | name = "redox_syscall" 428 | version = "0.1.40" 429 | source = "registry+https://github.com/rust-lang/crates.io-index" 430 | 431 | [[package]] 432 | name = "redox_termios" 433 | version = "0.1.1" 434 | source = "registry+https://github.com/rust-lang/crates.io-index" 435 | dependencies = [ 436 | "redox_syscall 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)", 437 | ] 438 | 439 | [[package]] 440 | name = "regex" 441 | version = "1.0.1" 442 | source = "registry+https://github.com/rust-lang/crates.io-index" 443 | dependencies = [ 444 | "aho-corasick 0.6.5 (registry+https://github.com/rust-lang/crates.io-index)", 445 | "memchr 2.0.1 (registry+https://github.com/rust-lang/crates.io-index)", 446 | "regex-syntax 0.6.1 (registry+https://github.com/rust-lang/crates.io-index)", 447 | "thread_local 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)", 448 | "utf8-ranges 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", 449 | ] 450 | 451 | [[package]] 452 | name = "regex-syntax" 453 | version = "0.6.1" 454 | source = "registry+https://github.com/rust-lang/crates.io-index" 455 | dependencies = [ 456 | "ucd-util 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", 457 | ] 458 | 459 | [[package]] 460 | name = "remove_dir_all" 461 | version = "0.5.1" 462 | source = "registry+https://github.com/rust-lang/crates.io-index" 463 | dependencies = [ 464 | "winapi 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)", 465 | ] 466 | 467 | [[package]] 468 | name = "rustc-demangle" 469 | version = "0.1.8" 470 | source = "registry+https://github.com/rust-lang/crates.io-index" 471 | 472 | [[package]] 473 | name = "scan_dir" 474 | version = "0.3.3" 475 | source = "registry+https://github.com/rust-lang/crates.io-index" 476 | dependencies = [ 477 | "quick-error 1.2.2 (registry+https://github.com/rust-lang/crates.io-index)", 478 | ] 479 | 480 | [[package]] 481 | name = "serde" 482 | version = "1.0.70" 483 | source = "registry+https://github.com/rust-lang/crates.io-index" 484 | 485 | [[package]] 486 | name = "serde_derive" 487 | version = "1.0.70" 488 | source = "registry+https://github.com/rust-lang/crates.io-index" 489 | dependencies = [ 490 | "proc-macro2 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)", 491 | "quote 0.6.3 (registry+https://github.com/rust-lang/crates.io-index)", 492 | "syn 0.14.4 (registry+https://github.com/rust-lang/crates.io-index)", 493 | ] 494 | 495 | [[package]] 496 | name = "serde_json" 497 | version = "1.0.22" 498 | source = "registry+https://github.com/rust-lang/crates.io-index" 499 | dependencies = [ 500 | "dtoa 0.4.3 (registry+https://github.com/rust-lang/crates.io-index)", 501 | "itoa 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)", 502 | "serde 1.0.70 (registry+https://github.com/rust-lang/crates.io-index)", 503 | ] 504 | 505 | [[package]] 506 | name = "sha2" 507 | version = "0.7.1" 508 | source = "registry+https://github.com/rust-lang/crates.io-index" 509 | dependencies = [ 510 | "block-buffer 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)", 511 | "byte-tools 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", 512 | "digest 0.7.4 (registry+https://github.com/rust-lang/crates.io-index)", 513 | "fake-simd 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", 514 | ] 515 | 516 | [[package]] 517 | name = "syn" 518 | version = "0.11.11" 519 | source = "registry+https://github.com/rust-lang/crates.io-index" 520 | dependencies = [ 521 | "quote 0.3.15 (registry+https://github.com/rust-lang/crates.io-index)", 522 | "synom 0.11.3 (registry+https://github.com/rust-lang/crates.io-index)", 523 | "unicode-xid 0.0.4 (registry+https://github.com/rust-lang/crates.io-index)", 524 | ] 525 | 526 | [[package]] 527 | name = "syn" 528 | version = "0.14.4" 529 | source = "registry+https://github.com/rust-lang/crates.io-index" 530 | dependencies = [ 531 | "proc-macro2 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)", 532 | "quote 0.6.3 (registry+https://github.com/rust-lang/crates.io-index)", 533 | "unicode-xid 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", 534 | ] 535 | 536 | [[package]] 537 | name = "synom" 538 | version = "0.11.3" 539 | source = "registry+https://github.com/rust-lang/crates.io-index" 540 | dependencies = [ 541 | "unicode-xid 0.0.4 (registry+https://github.com/rust-lang/crates.io-index)", 542 | ] 543 | 544 | [[package]] 545 | name = "synstructure" 546 | version = "0.6.1" 547 | source = "registry+https://github.com/rust-lang/crates.io-index" 548 | dependencies = [ 549 | "quote 0.3.15 (registry+https://github.com/rust-lang/crates.io-index)", 550 | "syn 0.11.11 (registry+https://github.com/rust-lang/crates.io-index)", 551 | ] 552 | 553 | [[package]] 554 | name = "tar" 555 | version = "0.4.16" 556 | source = "registry+https://github.com/rust-lang/crates.io-index" 557 | dependencies = [ 558 | "filetime 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", 559 | "libc 0.2.42 (registry+https://github.com/rust-lang/crates.io-index)", 560 | "redox_syscall 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)", 561 | "xattr 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", 562 | ] 563 | 564 | [[package]] 565 | name = "tempfile" 566 | version = "3.0.2" 567 | source = "registry+https://github.com/rust-lang/crates.io-index" 568 | dependencies = [ 569 | "libc 0.2.42 (registry+https://github.com/rust-lang/crates.io-index)", 570 | "rand 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)", 571 | "redox_syscall 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)", 572 | "remove_dir_all 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)", 573 | "winapi 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)", 574 | ] 575 | 576 | [[package]] 577 | name = "termcolor" 578 | version = "0.3.6" 579 | source = "registry+https://github.com/rust-lang/crates.io-index" 580 | dependencies = [ 581 | "wincolor 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)", 582 | ] 583 | 584 | [[package]] 585 | name = "termion" 586 | version = "1.5.1" 587 | source = "registry+https://github.com/rust-lang/crates.io-index" 588 | dependencies = [ 589 | "libc 0.2.42 (registry+https://github.com/rust-lang/crates.io-index)", 590 | "redox_syscall 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)", 591 | "redox_termios 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", 592 | ] 593 | 594 | [[package]] 595 | name = "thread_local" 596 | version = "0.3.5" 597 | source = "registry+https://github.com/rust-lang/crates.io-index" 598 | dependencies = [ 599 | "lazy_static 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)", 600 | "unreachable 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", 601 | ] 602 | 603 | [[package]] 604 | name = "time" 605 | version = "0.1.40" 606 | source = "registry+https://github.com/rust-lang/crates.io-index" 607 | dependencies = [ 608 | "libc 0.2.42 (registry+https://github.com/rust-lang/crates.io-index)", 609 | "redox_syscall 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)", 610 | "winapi 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)", 611 | ] 612 | 613 | [[package]] 614 | name = "typenum" 615 | version = "1.10.0" 616 | source = "registry+https://github.com/rust-lang/crates.io-index" 617 | 618 | [[package]] 619 | name = "ucd-util" 620 | version = "0.1.1" 621 | source = "registry+https://github.com/rust-lang/crates.io-index" 622 | 623 | [[package]] 624 | name = "unicase" 625 | version = "2.1.0" 626 | source = "registry+https://github.com/rust-lang/crates.io-index" 627 | dependencies = [ 628 | "version_check 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)", 629 | ] 630 | 631 | [[package]] 632 | name = "unicode-bidi" 633 | version = "0.3.4" 634 | source = "registry+https://github.com/rust-lang/crates.io-index" 635 | dependencies = [ 636 | "matches 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)", 637 | ] 638 | 639 | [[package]] 640 | name = "unicode-normalization" 641 | version = "0.1.7" 642 | source = "registry+https://github.com/rust-lang/crates.io-index" 643 | 644 | [[package]] 645 | name = "unicode-xid" 646 | version = "0.0.4" 647 | source = "registry+https://github.com/rust-lang/crates.io-index" 648 | 649 | [[package]] 650 | name = "unicode-xid" 651 | version = "0.1.0" 652 | source = "registry+https://github.com/rust-lang/crates.io-index" 653 | 654 | [[package]] 655 | name = "unreachable" 656 | version = "1.0.0" 657 | source = "registry+https://github.com/rust-lang/crates.io-index" 658 | dependencies = [ 659 | "void 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", 660 | ] 661 | 662 | [[package]] 663 | name = "url" 664 | version = "1.7.1" 665 | source = "registry+https://github.com/rust-lang/crates.io-index" 666 | dependencies = [ 667 | "idna 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)", 668 | "matches 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)", 669 | "percent-encoding 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)", 670 | ] 671 | 672 | [[package]] 673 | name = "utf8-ranges" 674 | version = "1.0.0" 675 | source = "registry+https://github.com/rust-lang/crates.io-index" 676 | 677 | [[package]] 678 | name = "vcpkg" 679 | version = "0.2.4" 680 | source = "registry+https://github.com/rust-lang/crates.io-index" 681 | 682 | [[package]] 683 | name = "version_check" 684 | version = "0.1.4" 685 | source = "registry+https://github.com/rust-lang/crates.io-index" 686 | 687 | [[package]] 688 | name = "void" 689 | version = "1.0.2" 690 | source = "registry+https://github.com/rust-lang/crates.io-index" 691 | 692 | [[package]] 693 | name = "winapi" 694 | version = "0.3.5" 695 | source = "registry+https://github.com/rust-lang/crates.io-index" 696 | dependencies = [ 697 | "winapi-i686-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", 698 | "winapi-x86_64-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", 699 | ] 700 | 701 | [[package]] 702 | name = "winapi-i686-pc-windows-gnu" 703 | version = "0.4.0" 704 | source = "registry+https://github.com/rust-lang/crates.io-index" 705 | 706 | [[package]] 707 | name = "winapi-x86_64-pc-windows-gnu" 708 | version = "0.4.0" 709 | source = "registry+https://github.com/rust-lang/crates.io-index" 710 | 711 | [[package]] 712 | name = "wincolor" 713 | version = "0.1.6" 714 | source = "registry+https://github.com/rust-lang/crates.io-index" 715 | dependencies = [ 716 | "winapi 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)", 717 | ] 718 | 719 | [[package]] 720 | name = "xattr" 721 | version = "0.2.2" 722 | source = "registry+https://github.com/rust-lang/crates.io-index" 723 | dependencies = [ 724 | "libc 0.2.42 (registry+https://github.com/rust-lang/crates.io-index)", 725 | ] 726 | 727 | [metadata] 728 | "checksum adler32 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)" = "7e522997b529f05601e05166c07ed17789691f562762c7f3b987263d2dedee5c" 729 | "checksum aho-corasick 0.6.5 (registry+https://github.com/rust-lang/crates.io-index)" = "f0ba20154ea1f47ce2793322f049c5646cc6d0fa9759d5f333f286e507bf8080" 730 | "checksum argparse 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "37bb99f5e39ee8b23b6e227f5b8f024207e8616f44aa4b8c76ecd828011667ef" 731 | "checksum arrayref 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)" = "0fd1479b7c29641adbd35ff3b5c293922d696a92f25c8c975da3e0acbc87258f" 732 | "checksum assert_cli 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)" = "98589b0e465a6c510d95fceebd365bb79bedece7f6e18a480897f2015f85ec51" 733 | "checksum atty 0.2.10 (registry+https://github.com/rust-lang/crates.io-index)" = "2fc4a1aa4c24c0718a250f0681885c1af91419d242f29eb8f2ab28502d80dbd1" 734 | "checksum backtrace 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)" = "89a47830402e9981c5c41223151efcced65a0510c13097c769cede7efb34782a" 735 | "checksum backtrace-sys 0.1.23 (registry+https://github.com/rust-lang/crates.io-index)" = "bff67d0c06556c0b8e6b5f090f0eac52d950d9dfd1d35ba04e4ca3543eaf6a7e" 736 | "checksum bitflags 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)" = "d0c54bb8f454c567f21197eefcdbf5679d0bd99f2ddbe52e84c77061952e6789" 737 | "checksum block-buffer 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "a076c298b9ecdb530ed9d967e74a6027d6a7478924520acddcddc24c1c8ab3ab" 738 | "checksum build_const 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "39092a32794787acd8525ee150305ff051b0aa6cc2abaf193924f5ab05425f39" 739 | "checksum byte-tools 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "560c32574a12a89ecd91f5e742165893f86e3ab98d21f8ea548658eb9eef5f40" 740 | "checksum byteorder 1.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "74c0b906e9446b0a2e4f760cdb3fa4b2c48cdc6db8766a845c54b6ff063fd2e9" 741 | "checksum cc 1.0.17 (registry+https://github.com/rust-lang/crates.io-index)" = "49ec142f5768efb5b7622aebc3fdbdbb8950a4b9ba996393cb76ef7466e8747d" 742 | "checksum cfg-if 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)" = "efe5c877e17a9c717a0bf3613b2709f723202c4e4675cc8f12926ded29bcb17e" 743 | "checksum cmake 0.1.31 (registry+https://github.com/rust-lang/crates.io-index)" = "95470235c31c726d72bf2e1f421adc1e65b9d561bf5529612cbe1a72da1467b3" 744 | "checksum colored 1.6.1 (registry+https://github.com/rust-lang/crates.io-index)" = "dc0a60679001b62fb628c4da80e574b9645ab4646056d7c9018885efffe45533" 745 | "checksum crc 1.8.1 (registry+https://github.com/rust-lang/crates.io-index)" = "d663548de7f5cca343f1e0a48d14dcfb0e9eb4e079ec58883b7251539fa10aeb" 746 | "checksum difference 2.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "524cbf6897b527295dff137cec09ecf3a05f4fddffd7dfcd1585403449e74198" 747 | "checksum digest 0.7.4 (registry+https://github.com/rust-lang/crates.io-index)" = "3cae2388d706b52f2f2f9afe280f9d768be36544bd71d1b8120cb34ea6450b55" 748 | "checksum dtoa 0.4.3 (registry+https://github.com/rust-lang/crates.io-index)" = "6d301140eb411af13d3115f9a562c85cc6b541ade9dfa314132244aaee7489dd" 749 | "checksum env_logger 0.5.10 (registry+https://github.com/rust-lang/crates.io-index)" = "0e6e40ebb0e66918a37b38c7acab4e10d299e0463fe2af5d29b9cc86710cfd2a" 750 | "checksum environment 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "1f4b14e20978669064c33b4c1e0fb4083412e40fe56cbea2eae80fd7591503ee" 751 | "checksum failure 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "934799b6c1de475a012a02dab0ace1ace43789ee4b99bcfbf1a2e3e8ced5de82" 752 | "checksum failure_derive 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "c7cdda555bb90c9bb67a3b670a0f42de8e73f5981524123ad8578aafec8ddb8b" 753 | "checksum fake-simd 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "e88a8acf291dafb59c2d96e8f59828f3838bb1a70398823ade51a84de6a6deed" 754 | "checksum filetime 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "da4b9849e77b13195302c174324b5ba73eec9b236b24c221a61000daefb95c5f" 755 | "checksum fuchsia-zircon 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "2e9763c69ebaae630ba35f74888db465e49e259ba1bc0eda7d06f4a067615d82" 756 | "checksum fuchsia-zircon-sys 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "3dcaa9ae7725d12cdb85b3ad99a434db70b468c09ded17e012d86b5c1010f7a7" 757 | "checksum generic-array 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ef25c5683767570c2bbd7deba372926a55eaae9982d7726ee2a1050239d45b9d" 758 | "checksum git2 0.7.2 (registry+https://github.com/rust-lang/crates.io-index)" = "910a2df52d2354e4eb27aa12f3803ea86bf461a93e17028908ec0e356572aa7b" 759 | "checksum humannum 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "2543f51658681860b1ad9aee0aac90182bc8b971f71a3d9eb1d38e23e1158a4b" 760 | "checksum humantime 1.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "0484fda3e7007f2a4a0d9c3a703ca38c71c54c55602ce4660c419fd32e188c9e" 761 | "checksum idna 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "38f09e0f0b1fb55fdee1f17470ad800da77af5186a1a76c026b679358b7e844e" 762 | "checksum itoa 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)" = "5adb58558dcd1d786b5f0bd15f3226ee23486e24b7b58304b60f64dc68e62606" 763 | "checksum lazy_static 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "e6412c5e2ad9584b0b8e979393122026cdd6d2a80b933f890dcd694ddbe73739" 764 | "checksum libc 0.2.42 (registry+https://github.com/rust-lang/crates.io-index)" = "b685088df2b950fccadf07a7187c8ef846a959c142338a48f9dc0b94517eb5f1" 765 | "checksum libflate 0.1.16 (registry+https://github.com/rust-lang/crates.io-index)" = "7d4b4c7aff5bac19b956f693d0ea0eade8066deb092186ae954fa6ba14daab98" 766 | "checksum libgit2-sys 0.7.4 (registry+https://github.com/rust-lang/crates.io-index)" = "7adce4cc6db027611f537837a7c404319b6314dae49c5db80ad5332229894751" 767 | "checksum libz-sys 1.0.18 (registry+https://github.com/rust-lang/crates.io-index)" = "87f737ad6cc6fd6eefe3d9dc5412f1573865bded441300904d2f42269e140f16" 768 | "checksum log 0.4.3 (registry+https://github.com/rust-lang/crates.io-index)" = "61bd98ae7f7b754bc53dca7d44b604f733c6bba044ea6f41bc8d89272d8161d2" 769 | "checksum matches 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)" = "100aabe6b8ff4e4a7e32c1c13523379802df0772b82466207ac25b013f193376" 770 | "checksum memchr 2.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "796fba70e76612589ed2ce7f45282f5af869e0fdd7cc6199fa1aa1f1d591ba9d" 771 | "checksum num-traits 0.1.43 (registry+https://github.com/rust-lang/crates.io-index)" = "92e5113e9fd4cc14ded8e499429f396a20f98c772a47cc8622a736e1ec843c31" 772 | "checksum num-traits 0.2.5 (registry+https://github.com/rust-lang/crates.io-index)" = "630de1ef5cc79d0cdd78b7e33b81f083cbfe90de0f4b2b2f07f905867c70e9fe" 773 | "checksum percent-encoding 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "31010dd2e1ac33d5b46a5b413495239882813e0369f8ed8a5e266f173602f831" 774 | "checksum pkg-config 0.3.11 (registry+https://github.com/rust-lang/crates.io-index)" = "110d5ee3593dbb73f56294327fe5668bcc997897097cbc76b51e7aed3f52452f" 775 | "checksum proc-macro2 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)" = "effdb53b25cdad54f8f48843d67398f7ef2e14f12c1b4cb4effc549a6462a4d6" 776 | "checksum quick-error 1.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "9274b940887ce9addde99c4eee6b5c44cc494b182b97e73dc8ffdcb3397fd3f0" 777 | "checksum quire 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "e985aa8dd08479edae35e6da999bc48311b1f5613bff6cfe9c4a90bc742afa1c" 778 | "checksum quote 0.3.15 (registry+https://github.com/rust-lang/crates.io-index)" = "7a6e920b65c65f10b2ae65c831a81a073a89edd28c7cce89475bff467ab4167a" 779 | "checksum quote 0.6.3 (registry+https://github.com/rust-lang/crates.io-index)" = "e44651a0dc4cdd99f71c83b561e221f714912d11af1a4dff0631f923d53af035" 780 | "checksum rand 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)" = "eba5f8cb59cc50ed56be8880a5c7b496bfd9bd26394e176bc67884094145c2c5" 781 | "checksum redox_syscall 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)" = "c214e91d3ecf43e9a4e41e578973adeb14b474f2bee858742d127af75a0112b1" 782 | "checksum redox_termios 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "7e891cfe48e9100a70a3b6eb652fef28920c117d366339687bd5576160db0f76" 783 | "checksum regex 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "13c93d55961981ba9226a213b385216f83ab43bd6ac53ab16b2eeb47e337cf4e" 784 | "checksum regex-syntax 0.6.1 (registry+https://github.com/rust-lang/crates.io-index)" = "05b06a75f5217880fc5e905952a42750bf44787e56a6c6d6852ed0992f5e1d54" 785 | "checksum remove_dir_all 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "3488ba1b9a2084d38645c4c08276a1752dcbf2c7130d74f1569681ad5d2799c5" 786 | "checksum rustc-demangle 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)" = "76d7ba1feafada44f2d38eed812bd2489a03c0f5abb975799251518b68848649" 787 | "checksum scan_dir 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "1ccb8e3d26f2775b128b053d1827feb90ed954ef97e3cb88c822f42e946c9155" 788 | "checksum serde 1.0.70 (registry+https://github.com/rust-lang/crates.io-index)" = "0c3adf19c07af6d186d91dae8927b83b0553d07ca56cbf7f2f32560455c91920" 789 | "checksum serde_derive 1.0.70 (registry+https://github.com/rust-lang/crates.io-index)" = "3525a779832b08693031b8ecfb0de81cd71cfd3812088fafe9a7496789572124" 790 | "checksum serde_json 1.0.22 (registry+https://github.com/rust-lang/crates.io-index)" = "84b8035cabe9b35878adec8ac5fe03d5f6bc97ff6edd7ccb96b44c1276ba390e" 791 | "checksum sha2 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)" = "9eb6be24e4c23a84d7184280d2722f7f2731fcdd4a9d886efbfe4413e4847ea0" 792 | "checksum syn 0.11.11 (registry+https://github.com/rust-lang/crates.io-index)" = "d3b891b9015c88c576343b9b3e41c2c11a51c219ef067b264bd9c8aa9b441dad" 793 | "checksum syn 0.14.4 (registry+https://github.com/rust-lang/crates.io-index)" = "2beff8ebc3658f07512a413866875adddd20f4fd47b2a4e6c9da65cd281baaea" 794 | "checksum synom 0.11.3 (registry+https://github.com/rust-lang/crates.io-index)" = "a393066ed9010ebaed60b9eafa373d4b1baac186dd7e008555b0f702b51945b6" 795 | "checksum synstructure 0.6.1 (registry+https://github.com/rust-lang/crates.io-index)" = "3a761d12e6d8dcb4dcf952a7a89b475e3a9d69e4a69307e01a470977642914bd" 796 | "checksum tar 0.4.16 (registry+https://github.com/rust-lang/crates.io-index)" = "e8f41ca4a5689f06998f0247fcb60da6c760f1950cc9df2a10d71575ad0b062a" 797 | "checksum tempfile 3.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "47776f63b85777d984a50ce49d6b9e58826b6a3766a449fc95bc66cd5663c15b" 798 | "checksum termcolor 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)" = "adc4587ead41bf016f11af03e55a624c06568b5a19db4e90fde573d805074f83" 799 | "checksum termion 1.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "689a3bdfaab439fd92bc87df5c4c78417d3cbe537487274e9b0b2dce76e92096" 800 | "checksum thread_local 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)" = "279ef31c19ededf577bfd12dfae728040a21f635b06a24cd670ff510edd38963" 801 | "checksum time 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)" = "d825be0eb33fda1a7e68012d51e9c7f451dc1a69391e7fdc197060bb8c56667b" 802 | "checksum typenum 1.10.0 (registry+https://github.com/rust-lang/crates.io-index)" = "612d636f949607bdf9b123b4a6f6d966dedf3ff669f7f045890d3a4a73948169" 803 | "checksum ucd-util 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "fd2be2d6639d0f8fe6cdda291ad456e23629558d466e2789d2c3e9892bda285d" 804 | "checksum unicase 2.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "284b6d3db520d67fbe88fd778c21510d1b0ba4a551e5d0fbb023d33405f6de8a" 805 | "checksum unicode-bidi 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)" = "49f2bd0c6468a8230e1db229cff8029217cf623c767ea5d60bfbd42729ea54d5" 806 | "checksum unicode-normalization 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)" = "6a0180bc61fc5a987082bfa111f4cc95c4caff7f9799f3e46df09163a937aa25" 807 | "checksum unicode-xid 0.0.4 (registry+https://github.com/rust-lang/crates.io-index)" = "8c1f860d7d29cf02cb2f3f359fd35991af3d30bac52c57d265a3c461074cb4dc" 808 | "checksum unicode-xid 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "fc72304796d0818e357ead4e000d19c9c174ab23dc11093ac919054d20a6a7fc" 809 | "checksum unreachable 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "382810877fe448991dfc7f0dd6e3ae5d58088fd0ea5e35189655f84e6814fa56" 810 | "checksum url 1.7.1 (registry+https://github.com/rust-lang/crates.io-index)" = "2a321979c09843d272956e73700d12c4e7d3d92b2ee112b31548aef0d4efc5a6" 811 | "checksum utf8-ranges 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "662fab6525a98beff2921d7f61a39e7d59e0b425ebc7d0d9e66d316e55124122" 812 | "checksum vcpkg 0.2.4 (registry+https://github.com/rust-lang/crates.io-index)" = "cbe533e138811704c0e3cbde65a818b35d3240409b4346256c5ede403e082474" 813 | "checksum version_check 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)" = "7716c242968ee87e5542f8021178248f267f295a5c4803beae8b8b7fd9bc6051" 814 | "checksum void 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d" 815 | "checksum winapi 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)" = "773ef9dcc5f24b7d850d0ff101e542ff24c3b090a9768e03ff889fdef41f00fd" 816 | "checksum winapi-i686-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 817 | "checksum winapi-x86_64-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 818 | "checksum wincolor 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)" = "eeb06499a3a4d44302791052df005d5232b927ed1a9658146d842165c4de7767" 819 | "checksum xattr 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "244c3741f4240ef46274860397c7c74e50eb23624996930e484c16679633a54c" 820 | --------------------------------------------------------------------------------