├── .envrc
├── src
├── repository
│ ├── mod.rs
│ ├── config.rs
│ ├── package.rs
│ └── repo.rs
├── internal
│ ├── mod.rs
│ ├── exit_codes.rs
│ ├── strings.rs
│ ├── structs.rs
│ └── read.rs
├── operations
│ ├── mod.rs
│ ├── config.rs
│ ├── clean.rs
│ ├── prune.rs
│ ├── build.rs
│ ├── clone.rs
│ ├── pull.rs
│ └── info.rs
├── args.rs
└── main.rs
├── .gitignore
├── examples
├── workspace
│ └── mlc.toml
└── repository
│ └── mlc.toml
├── .github
└── workflows
│ └── test.yml
├── docs
├── WORKSPACE_MODE.md
├── REPOSITORY_MODE.md
├── GETTING_STARTED.md
├── USAGE.md
└── COMMON_FEATURES.md
├── Cargo.toml
├── flake.nix
├── flake.lock
├── README.md
├── Cargo.lock
└── LICENSE
/.envrc:
--------------------------------------------------------------------------------
1 | use flake
2 |
--------------------------------------------------------------------------------
/src/repository/mod.rs:
--------------------------------------------------------------------------------
1 | pub use config::*;
2 | pub use package::*;
3 | pub use repo::*;
4 |
5 | mod config;
6 | mod package;
7 | mod repo;
8 |
--------------------------------------------------------------------------------
/src/internal/mod.rs:
--------------------------------------------------------------------------------
1 | pub use exit_codes::*;
2 | pub use read::*;
3 |
4 | mod exit_codes;
5 | mod read;
6 | pub mod strings;
7 | pub mod structs;
8 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /target
2 | .idea
3 | .direnv
4 |
5 | examples/workspace/*
6 | examples/repository/*
7 | !examples/workspace/mlc.toml
8 | !examples/repository/mlc.toml
--------------------------------------------------------------------------------
/src/internal/exit_codes.rs:
--------------------------------------------------------------------------------
1 | pub enum AppExitCode {
2 | #[cfg(target_os = "linux")]
3 | RunAsRoot = 1,
4 | PkgsNotFound = 2,
5 | DirNotEmpty = 3,
6 | ConfigParseError = 4,
7 | RepoParseError = 5,
8 | RepoNotClean = 6,
9 | }
10 |
--------------------------------------------------------------------------------
/src/operations/mod.rs:
--------------------------------------------------------------------------------
1 | pub use build::*;
2 | pub use clean::*;
3 | pub use clone::*;
4 | pub use config::*;
5 | pub use info::*;
6 | pub use prune::*;
7 | pub use pull::*;
8 |
9 | mod build;
10 | mod clean;
11 | mod clone;
12 | mod config;
13 | mod info;
14 | mod prune;
15 | mod pull;
16 |
--------------------------------------------------------------------------------
/examples/workspace/mlc.toml:
--------------------------------------------------------------------------------
1 | [base]
2 | mode = "workspace"
3 | smart_pull = true
4 |
5 | [mode.workspace]
6 | git_info = true
7 | colorblind = false
8 |
9 | [repositories]
10 | repos = [
11 | "crs:amethyst",
12 | "crs:malachite/development!",
13 | "aur:notop-git",
14 | "nix:nixpkgs/nixos-unstable:1",
15 | ]
16 |
17 | [repositories.urls]
18 | crs = "https://github.com/crystal-linux/{}"
19 | aur = "https://aur.archlinux.org/{}"
20 | nix = "https://github.com/nixos/{}"
21 |
--------------------------------------------------------------------------------
/src/operations/config.rs:
--------------------------------------------------------------------------------
1 | use std::env;
2 | use std::path::Path;
3 | use std::process::Command;
4 |
5 | use crate::{log, repository::create};
6 |
7 | pub fn config(verbose: bool) {
8 | // Generate new config file if not already present
9 | if !Path::exists("mlc.toml".as_ref()) {
10 | log!(verbose, "Creating mlc.toml");
11 | create(verbose);
12 | }
13 |
14 | // Open config file in user's editor of choice
15 | let editor = env::var("EDITOR").unwrap_or_else(|_| "nano".to_string());
16 | log!(verbose, "Opening mlc.toml in {}", editor);
17 | Command::new(editor)
18 | .arg("mlc.toml")
19 | .spawn()
20 | .unwrap()
21 | .wait()
22 | .unwrap();
23 | }
24 |
--------------------------------------------------------------------------------
/examples/repository/mlc.toml:
--------------------------------------------------------------------------------
1 | [base]
2 | mode = "repository"
3 | smart_pull = true
4 |
5 | [mode.repository]
6 | name = "repository-test"
7 | build_on_update = true
8 |
9 | [mode.repository.signing]
10 | enabled = true
11 | key = "michal@tar.black"
12 | on_gen = true
13 |
14 | [repositories]
15 | repos = [
16 | "crs:malachite/development:0a5bdc9", # Note, in this example, these two
17 | "mic:apod:v.1.1.2", # will fail to build.
18 | "pkg:pfetch!",
19 | "nms:rpass" # This too
20 | ]
21 |
22 | [repositories.urls]
23 | crs = "https://github.com/crystal-linux/{}"
24 | pkg = "https://github.com/crystal-linux/pkgbuild.{}"
25 | mic = "https://git.tar.black/michal/{}"
26 | nms = "https://github.com/not-my-segfault/{}"
27 |
--------------------------------------------------------------------------------
/.github/workflows/test.yml:
--------------------------------------------------------------------------------
1 | name: Lint Code
2 | on:
3 | workflow_dispatch:
4 | push:
5 | branches: [ main ]
6 | pull_request:
7 | branches: [ main ]
8 |
9 | env:
10 | AMETHYST_CODENAME: "Clippedy Clip"
11 |
12 | jobs:
13 | formatting:
14 | name: cargo fmt
15 | runs-on: ubuntu-latest
16 | steps:
17 | - uses: actions/checkout@v3
18 | - uses: actions-rust-lang/setup-rust-toolchain@v1
19 | with:
20 | components: rustfmt
21 | - name: Format
22 | uses: actions-rust-lang/rustfmt@v1.0.0
23 | clippy:
24 | name: cargo clippy
25 | runs-on: ubuntu-latest
26 | container: ghcr.io/crystal-linux/crystal:latest
27 | steps:
28 | - uses: actions/checkout@v1
29 | - run: |
30 | sudo pacman -Syu --needed --noconfirm
31 | sudo pacman -S --noconfirm rust
32 | - uses: actions-rs/clippy-check@v1
33 | with:
34 | token: ${{ secrets.GITHUB_TOKEN }}
--------------------------------------------------------------------------------
/docs/WORKSPACE_MODE.md:
--------------------------------------------------------------------------------
1 | # Workspace Mode
2 | You'll never have to work(space) another day in your life!
3 |
4 | ### Workspace Config
5 |
6 | Taking an example section from the Workspace mode config,
7 |
8 | ```toml
9 | [mode.workspace]
10 | git_info = true
11 | colorblind = true
12 | ```
13 |
14 | Currently, Workspace mode only has 2 options, both pertaining to the display of information. (`mlc info`)
15 |
16 | The first key is `git_info`, which is a boolean value. If it is true, the git information will be displayed alongside repository information.
17 |
18 | This information will be formatted as so: `D Pl Ps `
19 |
20 | The key for the values is as follows:
21 | - D: Whether the repository is dirty or not (unstaged changes)
22 | - Pl: Whether there are unpulled changes at the remote
23 | - Ps: Whether there are unpushed changes in your local repository
24 |
25 | These will be typically displayed in either Green (Clean) or Red (Dirty)
26 |
27 | However, if `colorblind` is set to true, the colors will instead be set to Blue (Clean) or Dark Red (Dirty), to be more discernible to colorblind users.
28 |
29 | ---
30 |
31 | You can return to [Getting Started](GETTING_STARTED.md) page here!
32 |
--------------------------------------------------------------------------------
/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "Malachite"
3 | version = "2.1.0"
4 | authors = ["michal "]
5 | edition = "2021"
6 | description = "Packaging tool for pacman repositories"
7 | repository = "https://github.com/crystal-linux/malachite"
8 | license-file = "LICENSE"
9 | keywords = ["pacman", "repository", "packaging"]
10 | categories = ["filesystem", "development-tools"]
11 |
12 | [[bin]]
13 | name = "mlc"
14 | path = "src/main.rs"
15 |
16 | [profile.release]
17 | incremental = true
18 | debug = false
19 | lto = "fat"
20 | codegen-units = 1
21 |
22 | [dependencies]
23 |
24 | clap = { version = "3.2.8", features = ["derive", "suggestions"] }
25 | toml = { version = "0.5.9", default-features = false }
26 | serde = { version = "1.0.139", default-features = false }
27 | serde_derive = { version = "1.0.139", default-features = false }
28 | libc = { version = "0.2.126", default-features = false }
29 | colored = { version = "2.0.0", default-features = false }
30 | tabled = { version = "0.8.0", default-features = false, features = [
31 | "derive",
32 | "color",
33 | ] }
34 | crossterm = { version = "0.25.0", default-features = false }
35 | regex = { version = "1.6.0", default-features = false, features = ["std"] }
36 | spinoff = { version = "0.5.4", default-features = false }
37 | rm_rf = { version = "0.6.2", default-features = false }
38 |
39 | [target.'cfg(target_os = "linux")'.dependencies]
40 | mimalloc = { version = "0.1.29" }
41 |
--------------------------------------------------------------------------------
/flake.nix:
--------------------------------------------------------------------------------
1 | {
2 | inputs = {
3 | nixpkgs.url = "github:nixos/nixpkgs";
4 | utils.url = "github:numtide/flake-utils";
5 | naersk = {
6 | url = "github:nix-community/naersk";
7 | inputs.nixpkgs.follows = "nixpkgs";
8 | };
9 | };
10 |
11 | outputs = {
12 | self,
13 | nixpkgs,
14 | utils,
15 | naersk,
16 | ...
17 | }:
18 | utils.lib.eachDefaultSystem (system: let
19 | pkgs = nixpkgs.legacyPackages."${system}";
20 | naersk-lib = naersk.lib."${system}";
21 | in rec
22 | {
23 | packages.malachite = naersk-lib.buildPackage {
24 | pname = "Malachite";
25 | root = ./.;
26 | };
27 |
28 | packages.default = packages.malachite;
29 |
30 | apps.malachite = utils.lib.mkApp {
31 | drv = packages.malachite;
32 | };
33 |
34 | apps.default = apps.malachite;
35 |
36 | devShells.default = pkgs.mkShell {
37 | nativeBuildInputs = with pkgs; [
38 | rustc
39 | cargo
40 | cargo-audit
41 | rustfmt
42 | clippy
43 | rust-analyzer
44 |
45 | # For `alpm` libs
46 | pkg-config
47 | pacman
48 | openssl
49 | ];
50 | # For rust-analyzer
51 | RUST_SRC_PATH = "${pkgs.rust.packages.stable.rustPlatform.rustLibSrc}";
52 | };
53 |
54 | formatter = pkgs.alejandra;
55 | });
56 | }
--------------------------------------------------------------------------------
/docs/REPOSITORY_MODE.md:
--------------------------------------------------------------------------------
1 | # Repository Mode
2 | PacManage your repositories in style!
3 |
4 | ### Repository Config
5 |
6 | As opposed to the rather barren Workspace mode, the Repository mode config is rather fleshed out;
7 | and we have a few options to choose from.
8 |
9 | Let's take an example section from a Repository mode config,
10 |
11 | ```toml
12 | [mode.repository]
13 | name = "example"
14 | build_on_update = true
15 |
16 | [mode.repository.signing]
17 | enabled = true
18 | key = "you@example.org"
19 | on_gen = true
20 | ```
21 |
22 | ### Basic Repository Config
23 |
24 | To start with, there are 2 main config keys to Repository mode:
25 | - `name`: Defines what pacman calls your repository.
26 | - `build_on_update`: In conjunction with `smart_pull`, defines whether to rebuild packages automatically when an update is detected.
27 |
28 | ### Signing
29 |
30 | Malachite also supports, and encourages, the signing of packages.
31 | GPG Signing packages ensures that the user receives exactly what you packaged, without any chance of tampering.
32 |
33 | Calling back to the example above, we can see 3 config keys:
34 |
35 | - `enabled`: Defines whether to sign packages (heavily encouraged).
36 | - `key`: Defines the GPG key ID to use for signing.
37 | - `on_gen`: Defines whether to sign packages when they are built, or all at once on repository generation (this is also recommended).
38 |
39 | ---
40 |
41 | You can return to [Getting Started](GETTING_STARTED.md) page here!
42 |
--------------------------------------------------------------------------------
/flake.lock:
--------------------------------------------------------------------------------
1 | {
2 | "nodes": {
3 | "naersk": {
4 | "inputs": {
5 | "nixpkgs": [
6 | "nixpkgs"
7 | ]
8 | },
9 | "locked": {
10 | "lastModified": 1655042882,
11 | "narHash": "sha256-9BX8Fuez5YJlN7cdPO63InoyBy7dm3VlJkkmTt6fS1A=",
12 | "owner": "nix-community",
13 | "repo": "naersk",
14 | "rev": "cddffb5aa211f50c4b8750adbec0bbbdfb26bb9f",
15 | "type": "github"
16 | },
17 | "original": {
18 | "owner": "nix-community",
19 | "repo": "naersk",
20 | "type": "github"
21 | }
22 | },
23 | "nixpkgs": {
24 | "locked": {
25 | "lastModified": 1664281702,
26 | "narHash": "sha256-haixZ4TJLu1Dciow54wrHrHvlGDVr5sW6MTeAV/ZLuI=",
27 | "owner": "nixos",
28 | "repo": "nixpkgs",
29 | "rev": "7e52b35fe98481a279d89f9c145f8076d049d2b9",
30 | "type": "github"
31 | },
32 | "original": {
33 | "owner": "nixos",
34 | "repo": "nixpkgs",
35 | "type": "github"
36 | }
37 | },
38 | "root": {
39 | "inputs": {
40 | "naersk": "naersk",
41 | "nixpkgs": "nixpkgs",
42 | "utils": "utils"
43 | }
44 | },
45 | "utils": {
46 | "locked": {
47 | "lastModified": 1656928814,
48 | "narHash": "sha256-RIFfgBuKz6Hp89yRr7+NR5tzIAbn52h8vT6vXkYjZoM=",
49 | "owner": "numtide",
50 | "repo": "flake-utils",
51 | "rev": "7e2a3b3dfd9af950a856d66b0a7d01e3c18aa249",
52 | "type": "github"
53 | },
54 | "original": {
55 | "owner": "numtide",
56 | "repo": "flake-utils",
57 | "type": "github"
58 | }
59 | }
60 | },
61 | "root": "root",
62 | "version": 7
63 | }
64 |
--------------------------------------------------------------------------------
/docs/GETTING_STARTED.md:
--------------------------------------------------------------------------------
1 | # Getting Started With Malachite
2 | Baby's first Malachite repository!
3 |
4 | ### What you need to know
5 |
6 | Malachite is:
7 | - A pacman repository manager
8 | - A workspace manager
9 | - ~~Awesome~~
10 |
11 | Malachite isn't:
12 | - The end-all solution for all pacman repositories
13 | - Perfect
14 |
15 |
16 | ### With that out of the way
17 |
18 | Hi! My name is Michal, and I wrote this tool pretty much on my own for [Crystal Linux](https://getcryst.al);
19 | but it is not at all exclusive to Crystal. This tool should and will work on and for any pacman-based
20 | distribution (so long as it packages all of Malachite's dependencies, of course).
21 |
22 | Throughout this tutorial, I'll explain each little feature of Malachite in what I hope to be bite-sized and
23 | programmatic chunks.
24 |
25 | Without further ado, let's begin with the first, most important question:
26 |
27 |
28 | ### Modes
29 |
30 | What mode are you using malachite in?
31 |
32 | Currently, malachite supports 2 modes:
33 |
34 | #### Repository Mode
35 | - Allows the user to configure and manage a remote (or local) pacman-based package repository
36 | - Allows for customisability in repository name, signing preferences, signing key etc.
37 | - Allows for basic levels of automation, by using features such as build_on_update
38 |
39 | #### Workspace Mode
40 | - The most basic functionality of Malachite
41 | - Just clones git directories into a "Workspace" directory for easier management
42 | - Allows for basic pulling operations to keep your repositories up-to-date
43 |
44 | These modes essentially dictate everything about how Malachite functions, so much so that I now need to
45 | split this page off before it gets too long!
46 |
47 | For more info, get started with the [Common Features](COMMON_FEATURES.md) page!
48 |
--------------------------------------------------------------------------------
/src/repository/config.rs:
--------------------------------------------------------------------------------
1 | use std::env;
2 | use std::fs::File;
3 | use std::io::Write;
4 | use std::path::Path;
5 |
6 | use crate::internal::AppExitCode;
7 | use crate::{crash, log};
8 |
9 | const DEFAULT_CONFIG: &str = r#"
10 | [base]
11 | # Either "repository" or "workspace"
12 | mode = ""
13 | # Better left as true, but can be set to false if it causes issues with branches
14 | smart_pull = true
15 |
16 | [mode.repository]
17 | # Decides what to call the repository and relevant files
18 | name = ""
19 | # Decides whether to build packages if package repo is updated on pull
20 | build_on_update = false
21 |
22 | [mode.repository.signing]
23 | # Decides whether or not to sign packages
24 | enabled = true
25 |
26 | [mode.workspace]
27 | # Whether to show rich git info for repositories
28 | git_info = true
29 | # Colorblind mode toggle
30 | colorblind = false
31 |
32 | [repositories]
33 | # List of repositories formatted as id:name (priority is decided by the ! suffix, and decides package build order)
34 | repos = [
35 | "aur:hello!",
36 | "crs:malachite"
37 | ]
38 |
39 | [repositories.urls]
40 | # URL keys for repositories, with {} where the repository name would go
41 | crs = "https://github.com/crystal-linux/{}"
42 | aur = "https://aur.archlinux.org/{}"
43 | "#;
44 |
45 | pub fn create(verbose: bool) {
46 | // Ensure current directory is empty
47 | if env::current_dir()
48 | .unwrap()
49 | .read_dir()
50 | .unwrap()
51 | .next()
52 | .is_some()
53 | {
54 | crash!(
55 | AppExitCode::DirNotEmpty,
56 | "Directory is not empty, please only create a repository in an empty directory"
57 | );
58 | }
59 | log!(verbose, "Creating config file");
60 |
61 | // If config file exists, create it
62 | if !Path::exists("mlc.toml".as_ref()) {
63 | let mut file = File::create("mlc.toml").unwrap();
64 | file.write_all(DEFAULT_CONFIG.as_ref()).unwrap();
65 | }
66 | log!(verbose, "Config file created");
67 | }
68 |
--------------------------------------------------------------------------------
/src/internal/strings.rs:
--------------------------------------------------------------------------------
1 | use colored::Colorize;
2 | use std::process::exit;
3 | use std::time::UNIX_EPOCH;
4 |
5 | use crate::internal::AppExitCode;
6 |
7 | const LOGO_SYMBOL: &str = "μ";
8 | const ERR_SYMBOL: &str = "❌";
9 |
10 | #[macro_export]
11 | macro_rules! info {
12 | ($($arg:tt)+) => {
13 | $crate::internal::strings::info_fn(&format!($($arg)+));
14 | }
15 | }
16 |
17 | #[macro_export]
18 | macro_rules! log {
19 | ($verbose:expr, $($arg:tt)+) => {
20 | $crate::internal::strings::log_fn(&format!("[{}:{}] {}", file!(), line!(), format!($($arg)+)), $verbose);
21 | }
22 | }
23 |
24 | #[macro_export]
25 | macro_rules! crash {
26 | ($exit_code:expr, $($arg:tt)+) => {
27 | $crate::internal::strings::crash_fn(&format!("[{}:{}] {}", file!(), line!(), format!($($arg)+)), $exit_code)
28 | }
29 | }
30 |
31 | #[macro_export]
32 | macro_rules! prompt {
33 | (default $default:expr, $($arg:tt)+) => {
34 | $crate::internal::strings::prompt_fn(&format!($($arg)+), $default)
35 | };
36 | }
37 |
38 | pub fn info_fn(msg: &str) {
39 | println!("{} {}", LOGO_SYMBOL.green(), msg.bold());
40 | }
41 |
42 | pub fn log_fn(msg: &str, verbose: bool) {
43 | if verbose {
44 | eprintln!(
45 | "{} {}",
46 | std::time::SystemTime::now()
47 | .duration_since(UNIX_EPOCH)
48 | .unwrap()
49 | .as_secs(),
50 | msg
51 | );
52 | }
53 | }
54 |
55 | pub fn crash_fn(msg: &str, exit_code: AppExitCode) {
56 | println!("{} {}", ERR_SYMBOL.red(), msg.bold());
57 | exit(exit_code as i32);
58 | }
59 |
60 | pub fn prompt_fn(msg: &str, default: bool) -> bool {
61 | let yn = if default { "[Y/n]" } else { "[y/N]" };
62 | print!("{} {} {}", "?".bold().green(), msg.bold(), yn);
63 | let mut input = String::new();
64 | std::io::stdin().read_line(&mut input).unwrap();
65 |
66 | let input = input.trim().to_lowercase();
67 |
68 | if input == "y" || input == "yes" {
69 | true
70 | } else if input == "n" || input == "no" {
71 | false
72 | } else {
73 | default
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/src/internal/structs.rs:
--------------------------------------------------------------------------------
1 | use serde_derive::Deserialize;
2 | use std::collections::HashMap;
3 |
4 | //// Config structs
5 | #[derive(Debug, Deserialize)]
6 | pub struct Config {
7 | pub base: ConfigBase,
8 | pub mode: ConfigMode,
9 | pub repositories: Vec,
10 | }
11 |
12 | #[derive(Debug, Deserialize)]
13 | pub struct UnexpandedConfig {
14 | pub base: ConfigBase,
15 | pub mode: ConfigMode,
16 | pub repositories: ConfigRepositories,
17 | }
18 |
19 | #[derive(Debug, Deserialize)]
20 | pub struct ConfigBase {
21 | pub mode: String,
22 | pub smart_pull: bool,
23 | }
24 |
25 | #[derive(Debug, Deserialize)]
26 | pub struct ConfigMode {
27 | pub repository: Option,
28 | pub workspace: Option,
29 | }
30 |
31 | #[derive(Debug, Deserialize)]
32 | pub struct ConfigModeRepository {
33 | pub name: String,
34 | pub build_on_update: bool,
35 | pub signing: ConfigModeRepositorySigning,
36 | }
37 |
38 | #[derive(Debug, Deserialize)]
39 | pub struct ConfigModeRepositorySigning {
40 | pub enabled: bool,
41 | pub key: Option,
42 | pub on_gen: Option,
43 | }
44 |
45 | #[derive(Debug, Deserialize)]
46 | pub struct ConfigModeWorkspace {
47 | pub git_info: bool,
48 | pub colorblind: bool,
49 | /* pub backup: bool,
50 | pub backup_dir: Option, TODO: Implement backup
51 | */
52 | }
53 |
54 | #[derive(Debug, Deserialize)]
55 | pub struct ConfigRepositories {
56 | pub repos: Vec,
57 | pub urls: HashMap,
58 | }
59 |
60 | #[derive(Debug, Deserialize)]
61 | pub struct ConfigRepositoriesExpanded {
62 | pub repos: Vec,
63 | }
64 |
65 | //// Repository structs
66 | #[derive(Debug, Deserialize)]
67 | pub struct Repo {
68 | pub name: String,
69 | pub url: String,
70 | pub branch: Option,
71 | pub extra: Option,
72 | pub priority: usize,
73 | }
74 |
75 | #[derive(Debug)]
76 | pub struct SplitRepo {
77 | pub id: String,
78 | pub name: String,
79 | pub extra: Option,
80 | }
81 |
82 | //// Build operation structs
83 | #[derive(Debug)]
84 | pub struct ErroredPackage {
85 | pub name: String,
86 | pub code: i32,
87 | }
88 |
--------------------------------------------------------------------------------
/src/repository/package.rs:
--------------------------------------------------------------------------------
1 | use std::path::Path;
2 | use std::process::Command;
3 | use std::{env, fs};
4 |
5 | use crate::internal::AppExitCode;
6 | use crate::{crash, log};
7 |
8 | pub fn build(pkg: &str, sign: bool, verbose: bool) -> i32 {
9 | log!(verbose, "Building {}", pkg);
10 | log!(verbose, "Signing: {}", sign);
11 |
12 | // Set root dir to return after build
13 | let dir = env::current_dir().unwrap();
14 | log!(verbose, "Root dir: {:?}", dir);
15 |
16 | // Create out dir if not already present
17 | if !Path::exists("out".as_ref()) {
18 | log!(verbose, "Creating out dir");
19 | fs::create_dir_all("out").unwrap();
20 | }
21 |
22 | // If package directory is not found, crash
23 | if !Path::exists(pkg.as_ref()) {
24 | crash!(
25 | AppExitCode::PkgsNotFound,
26 | "Repo for package {} not found, aborting",
27 | pkg
28 | );
29 | }
30 |
31 | // Enter build directory
32 | env::set_current_dir(pkg).unwrap();
33 | log!(verbose, "Current dir: {:?}", env::current_dir().unwrap());
34 |
35 | // If PKGBUILD is not found, return 63 and break
36 | if !Path::exists("PKGBUILD".as_ref()) {
37 | env::set_current_dir(&dir).unwrap();
38 | log!(verbose, "Current dir: {:?}", env::current_dir().unwrap());
39 | return 63;
40 | }
41 |
42 | // Parse extra flags from envvar
43 | let extra_flags = env::var("MAKEPKG_FLAGS").unwrap_or_else(|_| "".to_string());
44 | let extra_flags = extra_flags.split(' ').collect::>();
45 |
46 | // Default set of flags
47 | let default_args = vec![
48 | "-sf",
49 | "--skippgpcheck",
50 | if sign { "--sign" } else { "--nosign" },
51 | "--noconfirm",
52 | ];
53 |
54 | // Build each package
55 | let a = Command::new("makepkg")
56 | .args(
57 | default_args
58 | .iter()
59 | .chain(extra_flags.iter())
60 | .map(std::string::ToString::to_string),
61 | )
62 | .spawn()
63 | .unwrap()
64 | .wait()
65 | .unwrap();
66 | log!(verbose, "{} Build job returned: {:?}", pkg, a);
67 |
68 | // Copy built package to out dir
69 | Command::new("bash")
70 | .args(&["-c", "cp *.pkg.tar* ../out/"])
71 | .spawn()
72 | .unwrap()
73 | .wait()
74 | .unwrap();
75 | log!(verbose, "Copied built package to out dir");
76 |
77 | // Return to root dir
78 | env::set_current_dir(dir).unwrap();
79 | log!(
80 | verbose,
81 | "Returned to root dir: {:?}",
82 | env::current_dir().unwrap()
83 | );
84 |
85 | // Return exit code
86 | a.code().unwrap()
87 | }
88 |
--------------------------------------------------------------------------------
/src/operations/clean.rs:
--------------------------------------------------------------------------------
1 | use crate::{crash, info, internal::AppExitCode, log};
2 |
3 | pub fn clean(verbose: bool, force: bool) {
4 | info!("Resetting mlc repo, deleting all directories");
5 | // Get a vec of all files/dirs in the current directory
6 | let dir_paths = std::fs::read_dir(".").unwrap();
7 | log!(verbose, "Paths: {:?}", dir_paths);
8 | let mut dirs = dir_paths
9 | .map(|x| x.unwrap().path().display().to_string())
10 | .collect::>();
11 |
12 | // Remove mlc.toml and .git from output
13 | dirs.retain(|x| *x != "./mlc.toml" && *x != ".\\mlc.toml");
14 | dirs.retain(|x| *x != "./.git" && *x != ".\\.git");
15 | dirs.retain(|x| *x != "./.gitignore" && *x != ".\\.gitignore");
16 | dirs.retain(|x| *x != "./.gitmodules" && *x != ".\\.gitmodules");
17 | dirs.retain(|x| *x != "./README.md" && *x != ".\\README.md");
18 |
19 | let mut unclean_dirs = vec![];
20 |
21 | // Enter each directory and check git status
22 | for dir in &dirs {
23 | let root_dir = std::env::current_dir().unwrap();
24 |
25 | log!(verbose, "Entering directory: {}", dir);
26 | std::env::set_current_dir(dir).unwrap();
27 |
28 | let status = std::process::Command::new("git")
29 | .arg("status")
30 | .output()
31 | .unwrap();
32 |
33 | let output = std::string::String::from_utf8(status.stdout).unwrap();
34 | log!(verbose, "Git status: {}", output);
35 |
36 | if output.contains("Your branch is up to date with")
37 | && !output.contains("Untracked files")
38 | && !output.contains("Changes not staged for commit")
39 | {
40 | log!(verbose, "Directory {} is clean", dir);
41 | } else {
42 | unclean_dirs.push(dir);
43 | }
44 |
45 | std::env::set_current_dir(&root_dir).unwrap();
46 | log!(verbose, "Current directory: {}", root_dir.display());
47 | }
48 |
49 | if !unclean_dirs.is_empty() && !force && crate::parse_cfg(verbose).base.mode == "workspace" {
50 | crash!(
51 | AppExitCode::RepoNotClean,
52 | "The following directories are not clean: \n {}\n\
53 | If you are sure no important changes are staged, run `mlc clean` with the `--force` flag to delete them.",
54 | unclean_dirs.iter().map(|x| (*x).to_string().replace("./", "").replace(".\\", "")).collect::>().join(", ")
55 | );
56 | }
57 |
58 | log!(verbose, "Paths with mlc.toml excluded: {:?}", dirs);
59 | for dir in &dirs {
60 | log!(verbose, "Deleting directory: {}", dir);
61 | rm_rf::remove(dir).unwrap();
62 | }
63 | info!(
64 | "Reset complete, dirs removed: \n \
65 | {}",
66 | dirs.iter()
67 | .map(|x| x.replace("./", "").replace(".\\", ""))
68 | .collect::>()
69 | .join("\n ")
70 | );
71 | }
72 |
--------------------------------------------------------------------------------
/src/args.rs:
--------------------------------------------------------------------------------
1 | use clap::{ArgAction, Parser, Subcommand};
2 |
3 | #[derive(Debug, Clone, Parser)]
4 | #[clap(name = "Malachite", version = env ! ("CARGO_PKG_VERSION"), about = env ! ("CARGO_PKG_DESCRIPTION"))]
5 | pub struct Args {
6 | #[clap(subcommand)]
7 | pub subcommand: Option,
8 |
9 | /// Sets the level of verbosity
10 | #[clap(long, short, global(true), action = ArgAction::SetTrue)]
11 | pub verbose: bool,
12 |
13 | /// Excludes packages from given operation, if applicable
14 | #[clap(short = 'x', long = "exclude", action = ArgAction::Append, takes_value = true)]
15 | pub exclude: Vec,
16 | }
17 |
18 | #[derive(Debug, Clone, Subcommand)]
19 | pub enum Operation {
20 | /// Builds the given packages
21 | #[clap(name = "build", aliases = & ["b"])]
22 | Build {
23 | /// The packages to operate on
24 | #[clap(name = "package(s)", action = ArgAction::Append, index = 1)]
25 | packages: Vec,
26 |
27 | /// Does not regenerate repository after building given package(s)
28 | #[clap(short = 'n', long = "no-regen", action = ArgAction::SetTrue)]
29 | no_regen: bool,
30 | },
31 |
32 | /// Generates Pacman repository from built packages
33 | #[clap(name = "repo-gen", aliases = & ["repo", "r"])]
34 | RepoGen,
35 |
36 | /// Clones all git repositories from mlc.toml branching from current directory
37 | #[clap(name = "clone", aliases = & ["init", "i", "c"])]
38 | Clone,
39 |
40 | /// Removes everything in directory except for mlc.toml
41 | #[clap(name = "clean", aliases = & ["clean", "cl", "reset"])]
42 | Clean {
43 | /// Force removes everything, even if git directory is dirty or has unpushed changes or changes at remote
44 | #[clap(short = 'f', long = "force", action = ArgAction::SetTrue)]
45 | force: bool,
46 | },
47 |
48 | /// Removes all but the latest 3 versions of each package in a repository
49 | #[clap(name = "prune", aliases = & ["prune", "p"])]
50 | Prune,
51 |
52 | /// Shows an info panel/overview about the current repository
53 | #[clap(name = "info", aliases = & ["status", "s", "i"])]
54 | Info,
55 |
56 | /// Pulls in git repositories from mlc.toml branching from current directory
57 | #[clap(name = "pull", aliases = & ["u"])]
58 | Pull {
59 | /// The packages to operate on
60 | #[clap(name = "package(s)", help = "The packages to operate on", action = ArgAction::Append, index = 1)]
61 | packages: Vec,
62 |
63 | /// Does not regenerate repository after pulling given package(s). This only applies if build_on_update is set to true in repository config
64 | #[clap(short = 'n', long = "no-regen", action = ArgAction::SetTrue)]
65 | no_regen: bool,
66 |
67 | /// Will prompt for confirmation before rebuilding a package
68 | #[clap(long, action = ArgAction::SetTrue)]
69 | interactive: bool,
70 | },
71 |
72 | /// Create and/or open local config file
73 | #[clap(name = "config", aliases = & ["conf"])]
74 | Config,
75 | }
76 |
--------------------------------------------------------------------------------
/src/main.rs:
--------------------------------------------------------------------------------
1 | #![warn(clippy::all, clippy::pedantic, clippy::nursery, clippy::cargo)]
2 | #![allow(clippy::too_many_lines)]
3 |
4 | use clap::Parser;
5 |
6 | use crate::args::{Args, Operation};
7 | use crate::internal::parse_cfg;
8 | use crate::internal::AppExitCode;
9 |
10 | #[cfg(target_os = "linux")]
11 | #[global_allocator]
12 | static GLOBAL: mimalloc::MiMalloc = mimalloc::MiMalloc;
13 |
14 | mod args;
15 | mod internal;
16 | mod operations;
17 | mod repository;
18 |
19 | fn repository(verbose: bool) -> bool {
20 | // Parse config
21 | let config = parse_cfg(verbose);
22 | log!(verbose, "Config: {:?}", config);
23 |
24 | // Get repository mode status
25 | let repository = config.base.mode == "repository";
26 | log!(verbose, "Repository Mode: {:?}", repository);
27 |
28 | // Return repository mode status
29 | repository
30 | }
31 |
32 | fn main() {
33 | #[cfg(target_os = "linux")]
34 | if unsafe { libc::geteuid() } == 0 {
35 | crash!(AppExitCode::RunAsRoot, "Running malachite as root is disallowed as it can lead to system breakage. Instead, malachite will prompt you when it needs superuser permissions");
36 | }
37 |
38 | // Get required variables
39 | let args: Args = Args::parse();
40 | let exclude = &args.exclude;
41 | let verbose = args.verbose;
42 | log!(verbose, "Args: {:?}", args);
43 | log!(verbose, "Exclude: {:?}", exclude);
44 | log!(verbose, "Verbose: You guess. :)");
45 |
46 | // Arg matching
47 | match args.subcommand.unwrap_or(Operation::Clone) {
48 | Operation::Clone => operations::clone(verbose),
49 | Operation::Build {
50 | packages, no_regen, ..
51 | } => {
52 | if !repository(verbose) {
53 | crash!(
54 | AppExitCode::ConfigParseError,
55 | "Cannot build packages in workspace mode"
56 | );
57 | }
58 | operations::build(&packages, exclude.clone(), no_regen, verbose);
59 | }
60 | Operation::Pull {
61 | packages,
62 | no_regen,
63 | interactive,
64 | ..
65 | } => operations::pull(packages, exclude, verbose, no_regen, interactive),
66 | Operation::RepoGen => {
67 | if !repository(verbose) {
68 | crash!(
69 | AppExitCode::ConfigParseError,
70 | "Cannot generate repository in workspace mode"
71 | );
72 | }
73 | repository::generate(verbose);
74 | }
75 | Operation::Config => operations::config(verbose),
76 | Operation::Prune => {
77 | if !repository(verbose) {
78 | crash!(
79 | AppExitCode::ConfigParseError,
80 | "Cannot prune packages in workspace mode"
81 | );
82 | }
83 | operations::prune(verbose);
84 | }
85 | Operation::Clean { force, .. } => operations::clean(verbose, force),
86 | Operation::Info => operations::info(verbose),
87 | }
88 | }
89 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Malachite
8 |
9 | **NOTE**: As a trial, Malachite's development currently takes place at https://git.getcryst.al/crystal/software/malachite
10 |
11 |
12 |
13 |
14 |
15 | 
16 |
17 | 
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 | Malachite is a simple yet useful workspace and local repository management tool, made for packagers of Arch Linux based distributions.
26 |
27 |
28 |
29 |
30 |
31 | ### Basic Usage Guide
32 |
33 | | Action | Command |
34 | |--------------------------------------------------------|-------------------------------------------|
35 | | Build a package | mlc build \ [all if left empty] |
36 | | Generate local repository | mlc repo-gen |
37 | | Update local repos/PKGBUILDs | mlc pull/update [all if left empty] |
38 | | Create and/or open config file | mlc conf |
39 | | Initialises repo/workspace based on config in mlc.toml | mlc clone/init |
40 | | Displays information about a Malachite repository | mlc info/status |
41 |
42 | ### Pacman Repository Creation
43 |
44 | - `mlc config` to create the config (and also populate it)
45 | - `mlc init` to build repository base from config file
46 | - `mlc build ` to either build individual packages, or don't specify package names to build all packages in mlc.toml
47 | - `build` typically automatically updates the repository unless `--no-regen` is passed, if so:
48 | - `mlc repo-gen` to generate functional pacman repository at \/\.db from built packages
49 |
50 |
51 | ## How to build:
52 |
53 | Tested on latest Cargo (1.60.0-nightly)
54 |
55 | ### Debug/development builds
56 |
57 | - `cargo build`
58 |
59 | ### Optimised/release builds
60 |
61 | - `cargo build --release`
62 |
--------------------------------------------------------------------------------
/docs/USAGE.md:
--------------------------------------------------------------------------------
1 | # Detailed Usage
2 | Work it harder, make it better!
3 |
4 | ### Global Flags
5 |
6 | | Flag | Description |
7 | |-------------------|----------------------------------------------------------------------------------------------------------------------------------------|
8 | | `--verbose`, `-v` | Prints lots of debug information to `stderr`. If something doesn't go right, sending us the output with this enabled will help greatly |
9 | | `--exclude`, `-x` | Excludes the supplied package from the current operation. Can be used multiple times. |
10 |
11 | ### Basic Commands
12 |
13 | | Action | Command | Extra Flags |
14 | |-----------------------------------------------------------------------------------------|-------------------------------------------|------------------------------------------------------------------------------------------------------------------|
15 | | Build a package/packages. | `mlc build ` [all if left empty] | `--no-regen`: Doesn't regenerate repository after build |
16 | | Generate pacman repository | `mlc repo-gen` | |
17 | | Update local repos/PKGBUILDs | `mlc pull/update` [all if left empty] | `--no-regen`: If `mode.repository.build_on_update` is `true`, Do not regenerate repository after package rebuild |
18 | | Create and/or open config file | `mlc conf` | |
19 | | Initialises repo/workspace based on config in mlc.toml | `mlc clone/init` | |
20 | | Displays an info panel/overview of the current repo | `mlc info/status` | |
21 | | Resets Malachite repository by deleting all directories, omitting `mlc.toml` and `.git` | `mlc clean/reset` | `--force`: Remove dirty directories (unstaged, untracked, etc) |
22 |
23 | ### Exit Codes
24 |
25 | | AppExitCode (named Enum) | Exit code (i32) | Error Description |
26 | |--------------------------|-----------------|--------------------------------------------------------------------------------------------------------|
27 | | `RunAsRoot` | `1` | Malachite was run as root. This is highly discouraged. So highly, in fact, that it will refuse to run. |
28 | | `PkgsNotFound` | `2` | No packages were specified/found for the desired operation |
29 | | `DirNotEmpty` | `3` | The creation of a Malachite repository was attempted in a non-empty directory |
30 | | `ConfigParseError` | `4` | The config file could not be parsed |
31 | | `RepoParseError` | `5` | The repository info could not be parsed |
32 | | `RepoNotClean` | `6` | The git repository is not clean and cannot be removed without `--force` |
33 |
--------------------------------------------------------------------------------
/src/internal/read.rs:
--------------------------------------------------------------------------------
1 | use std::fs;
2 | use std::path::Path;
3 |
4 | use crate::internal::structs::{Config, Repo, SplitRepo, UnexpandedConfig};
5 | use crate::internal::AppExitCode;
6 | use crate::{crash, log};
7 |
8 | pub fn parse_cfg(verbose: bool) -> Config {
9 | // Crash if mlc.toml doesn't exist
10 | if !Path::exists("mlc.toml".as_ref()) {
11 | crash!(
12 | AppExitCode::ConfigParseError,
13 | "Config file not found (mlc.toml)"
14 | );
15 | }
16 |
17 | // Reading the config file to an UnexpandedConfig struct
18 | let file = fs::read_to_string("mlc.toml").unwrap();
19 | let config: UnexpandedConfig = toml::from_str(&file).unwrap_or_else(|e| {
20 | crash!(
21 | AppExitCode::ConfigParseError,
22 | "Error parsing config file: {}",
23 | e
24 | );
25 | // This is unreachable, but rustc complains about it otherwise
26 | std::process::exit(1);
27 | });
28 |
29 | log!(verbose, "Config file read: {:?}", config);
30 |
31 | // Crash if incorrect mode is set
32 | if config.base.mode != "workspace" && config.base.mode != "repository" {
33 | crash!(
34 | AppExitCode::ConfigParseError,
35 | "Invalid mode in mlc.toml, must be either \"repository\" or \"workspace\""
36 | );
37 | }
38 |
39 | let mut expanded_repos: Vec = vec![];
40 |
41 | // Parsing repos from the config file
42 | for x in config.repositories.repos {
43 | log!(verbose, "Parsing repo: {:?}", x);
44 | // Splits the repo name and index into a SplitRepo struct
45 | let split: Vec<&str> = x.split(':').collect();
46 | let split_struct = if split.len() > 2 {
47 | SplitRepo {
48 | id: split[0].parse().unwrap(),
49 | name: split[1].parse().unwrap(),
50 | extra: Some(split[2].parse().unwrap()),
51 | }
52 | } else {
53 | SplitRepo {
54 | id: split[0].parse().unwrap(),
55 | name: split[1].parse().unwrap(),
56 | extra: None,
57 | }
58 | };
59 | log!(verbose, "Split repo: {:?}", split_struct);
60 |
61 | // Parses all necessary values for expanding the repo to a Repo struct
62 | let id = split_struct.id;
63 |
64 | // If a branch is defined, parse it
65 | let branch = if split_struct.name.contains('/') {
66 | log!(verbose, "Branch defined: {}", split_struct.name);
67 | Some(
68 | split_struct.name.split('/').collect::>()[1]
69 | .to_string()
70 | .replace('!', ""),
71 | )
72 | } else {
73 | log!(verbose, "No branch defined");
74 | None
75 | };
76 |
77 | // Strip branch and priority info from the name, if present
78 | let name = if split_struct.name.contains('/') {
79 | split_struct.name.split('/').collect::>()[0].to_string()
80 | } else {
81 | split_struct.name.to_string().replace('!', "")
82 | };
83 |
84 | // Substitutes the name into the url
85 | let urls = &config.repositories.urls;
86 | let mut urls_vec = vec![];
87 | for (i, url) in urls {
88 | if i == &id {
89 | log!(verbose, "Substituting url: {:?}", url);
90 | urls_vec.push(url);
91 | }
92 | }
93 | let url = urls_vec[0].replace("{}", &name);
94 |
95 | // Counts instances of ! in the name, and totals a priority accordingly
96 | let priority = &split_struct.name.matches('!').count();
97 |
98 | // Creates and pushes Repo struct to expanded_repos
99 | let repo = Repo {
100 | name,
101 | url,
102 | branch,
103 | extra: split_struct.extra,
104 | priority: *priority,
105 | };
106 | log!(verbose, "Expanded repo: {:?}", repo);
107 | expanded_repos.push(repo);
108 | }
109 |
110 | // Returns parsed config file
111 | let conf = Config {
112 | base: config.base,
113 | mode: config.mode,
114 | repositories: expanded_repos,
115 | };
116 | log!(verbose, "Config: {:?}", conf);
117 |
118 | conf
119 | }
120 |
--------------------------------------------------------------------------------
/docs/COMMON_FEATURES.md:
--------------------------------------------------------------------------------
1 | # Common Features Between Modes
2 | As [mode]us, shared of between uh… repositories… or something.
3 |
4 | ### What you need to know
5 | Malachite is fairly fleshed out in Repository mode, and not so much in Workspace mode.
6 |
7 | This isn't of course because I'm lazy and hate Workspace mode or anything, there's just not
8 | a lot *to add*.
9 |
10 | Without further ado, let's take a look at this example config file.
11 |
12 | ```toml
13 | # mlc.toml
14 |
15 | [base]
16 | mode = "workspace"
17 | smart_pull = true
18 |
19 | [mode.workspace]
20 | git_info = true
21 | colorblind = true
22 |
23 | [repositories]
24 | repos = [
25 | "foo:repo1:2",
26 | "foo:repo2/testing",
27 | "bar:baz!",
28 | "bar:qux/testing!:1",
29 | ]
30 |
31 | [repositories.urls]
32 | foo = "https://example.org/{}.git"
33 | bar = "https://example.org/other/{}.git"
34 | ```
35 |
36 | Now, this is going to look really confusing at first, but bear with me.
37 |
38 | In this document, we'll cover only what is required to know for **both** modes.
39 | More specialized tutorials will be linked for each mode at the bottom of this page.
40 |
41 | Let's start with the base(ics).
42 |
43 |
44 | ### Base Config
45 | The base config defines a few common parameters between all the Malachite modes.
46 |
47 | ```toml
48 | [base]
49 | mode = "workspace"
50 | smart_pull = true
51 | ```
52 |
53 | In this snippet, we define `mode` to be `"workspace"`.
54 |
55 | `base.mode` in Malachite can only ever be one of `"workspace"` or `"repository"`, and defines, drumroll…
56 | The mode in which it operates. If it is set to anything but those 2 modes, it crashes.
57 |
58 | Also defined in this snippet is `smart_pull`, which controls whether to pull… smartly.
59 |
60 | What that actually means is that instead of just performing a simple `git pull` in each repository, Malachite
61 | will:
62 |
63 | - First run `git remote update` to fetch new remotes from origin
64 | - Then run `git status` and parse the output to see if the current branch is behind
65 | - If the current branch is behind, it runs a regular `git pull`, which will take advantage of the remotes
66 | already being updated.
67 |
68 | Theoretically, this only actually speeds things up by a minute amount (think milliseconds, really). Where this feature shines however is in repository mode,
69 | where it enables helpful automation features such as `build_on_update`.
70 |
71 | Regardless, it's recommended to keep this enabled for the slight speed-up, and only disable it if it causes issues.
72 | I've never personally had issues with it in the past, but who knows what could happen. This is Git we're talking about.
73 |
74 |
75 | ### Repositories Config
76 |
77 | The repositories config is realistically what makes Malachite churn repo butter internally. It's the whole
78 | purpose of what it does, and because of that we've tried to come up with a neat little system to help
79 | facilitate many packages without having to type each URL out a million times.
80 |
81 | ```toml
82 | [repositories]
83 | repos = [
84 | "foo:repo1:2",
85 | "foo:repo2/testing",
86 | "bar:baz!",
87 | "bar:qux/testing!:1",
88 | ]
89 |
90 | [repositories.urls]
91 | foo = "https://example.org/{}.git"
92 | bar = "https://example.org/other/{}.git"
93 | ```
94 |
95 | The way this works is simple:
96 | - We have 2 URLs in the `repositories.urls` key.
97 | - Each `repo` in the `repositories.repos` key is prefixed with an identifier.
98 | - If the number is `foo`, it'll insert the URL with the id `foo`.
99 | - Specifically, in the URL, it'll insert the defined `repo`'s name in place of the `%repo%` substring.
100 |
101 | #### Hang on, what are the special symbols????
102 |
103 | I'm glad you asked!
104 | - If you want to clone a specific branch, simply use the `/` delimiter. To clone repository `foo` on branch `bar`, use `id:foo/bar`.
105 | - If you want a specific package to build first, use instances of `!` to set priority. This is explained later in the [Repository Mode](REPOSITORY_MODE.md) page
106 |
107 | The last `:` delimiter is entirely optional, and behaves differently depending on the mode:
108 | - In Repository mode, it defines the desired commit hash/rev/tag to checkout on repository clone
109 | - In Workspace mode, it defines the desired depth to clone the repository, useful with large git repositories, such as `nixpkgs`.
110 |
111 | That's literally it!
112 |
113 |
114 | ### Mode-Specific Config
115 |
116 | For mode-specific config, avert your eyes to the following links!
117 |
118 | - [Workspace Mode](WORKSPACE_MODE.md)
119 | - [Repository Mode](REPOSITORY_MODE.md)
120 |
121 | ### Examples
122 |
123 | Functioning config examples for both modes are available in the [examples](../examples) directory!
124 |
125 | ### Usage
126 |
127 | Alternatively, you can look at the [Usage](USAGE.md) guide!
128 |
129 |
--------------------------------------------------------------------------------
/src/operations/prune.rs:
--------------------------------------------------------------------------------
1 | use colored::Colorize;
2 | use std::env;
3 | use std::fs;
4 | use std::path::PathBuf;
5 |
6 | use crate::info;
7 | use crate::log;
8 | use crate::parse_cfg;
9 |
10 | #[derive(Debug, Clone)]
11 | struct PackageFile {
12 | name: String,
13 | ver: String,
14 | ext: String,
15 | }
16 |
17 | pub fn prune(verbose: bool) {
18 | // Read config struct from mlc.toml
19 | let config = parse_cfg(verbose);
20 | log!(verbose, "Config: {:?}", config);
21 |
22 | // Read current directory
23 | let current_dir = env::current_dir().unwrap();
24 | log!(verbose, "Current dir: {:?}", current_dir);
25 |
26 | // Enter out directory
27 | env::set_current_dir("out").unwrap();
28 | log!(verbose, "Current dir: {:?}", env::current_dir().unwrap());
29 |
30 | // Read all files from . into a Vec, except for .sig files
31 | let mut files: Vec = vec![];
32 | for entry in fs::read_dir(".").unwrap() {
33 | let entry = entry.unwrap();
34 | let path = entry.path();
35 | if path.extension().unwrap() != "sig" {
36 | files.push(path);
37 | }
38 | }
39 | log!(verbose, "Files: {:?}", files);
40 |
41 | // Split files into Vec, turning package-name-1.0.0-1-x86_64.tar.gz into PackageFile { name: "package-name", ver: "1.0.0-1", ext: "x86_64.tar.gz" }
42 | let mut packages: Vec = vec![];
43 | for file in files {
44 | // Regex, splits package-name-1.0.0-1-x86_64.tar.gz into 3 groups: package-name, -1.0.0-1, -x86_64.tar.gz
45 | let re = regex::Regex::new(r"^(.+)(-.+-.+)(-.+\..+\..+\.+..+)$").unwrap();
46 |
47 | // Get file name to string
48 | let file = file.to_str().unwrap();
49 |
50 | // Match file name against regex
51 | for cap in re.captures_iter(file) {
52 | // Collect regex captures
53 | let name = cap[1].to_string();
54 | let mut ver = cap[2].to_string();
55 | let mut ext = cap[3].to_string();
56 |
57 | // Strip leading - from ver and ext
58 | ver.remove(0).to_string();
59 | ext.remove(0).to_string();
60 |
61 | let package = PackageFile { name, ver, ext };
62 | log!(verbose, "Package: {:?}", package);
63 | packages.push(package);
64 | }
65 | }
66 |
67 | // Split packages into a Vector of Vectors by unique name
68 | let mut packages_by_name: Vec> = vec![];
69 | for package in &packages {
70 | log!(verbose, "Sorting Package: {:?}", package);
71 | let name = &package.name;
72 | let mut found = false;
73 | // Check if name is already present in packages_by_name
74 | for p in &mut packages_by_name {
75 | if &p[0].name == name {
76 | log!(verbose, "Found {}", name);
77 | found = true;
78 | p.push(package);
79 | }
80 | }
81 | // If not, create a new vector and push to it
82 | if !found {
83 | log!(verbose, "Creating {}", name);
84 | packages_by_name.push(vec![package]);
85 | }
86 | }
87 |
88 | // Sort each Vector of Vectors by version
89 | for p in &mut packages_by_name {
90 | log!(verbose, "Sorting {:?}", p);
91 | p.sort_by(|a, b| b.ver.cmp(&a.ver));
92 | }
93 |
94 | // Pushes all but the 3 most recent versions of each package into a new Vector of PackageFiles
95 | let mut packages_to_delete: Vec = vec![];
96 | for p in &packages_by_name {
97 | let mut to_delete = vec![];
98 | for (i, _) in p.iter().enumerate() {
99 | if i >= 3 {
100 | log!(verbose, "Deleting {:?}", p[i]);
101 | to_delete.push(p[i].clone());
102 | }
103 | }
104 | packages_to_delete.extend(to_delete);
105 | }
106 | log!(verbose, "Packages to delete: {:?}", packages_to_delete);
107 |
108 | // Delete all packages in packages_to_delete
109 | for p in &packages_to_delete {
110 | let path = format!("{}-{}-{}", p.name, p.ver, p.ext);
111 | log!(verbose, "Deleting {}", path);
112 | std::process::Command::new("bash")
113 | .args(&["-c", &format!("rm -rf ./{} ./{}.sig", path, path)])
114 | .output()
115 | .unwrap();
116 | }
117 |
118 | // Return to current directory
119 | env::set_current_dir(current_dir).unwrap();
120 | log!(verbose, "Current dir: {:?}", env::current_dir().unwrap());
121 |
122 | // Print which packages were deleted
123 | if packages_to_delete.is_empty() {
124 | info!("No packages were deleted.");
125 | } else {
126 | info!("Deleted the following packages:");
127 | for p in &mut packages_to_delete {
128 | println!(
129 | "{}",
130 | format!(
131 | " {}-{}",
132 | p.name.replace("./", "").replace(".\\", ""),
133 | p.ver
134 | )
135 | .bold()
136 | );
137 | }
138 | }
139 | }
140 |
--------------------------------------------------------------------------------
/src/operations/build.rs:
--------------------------------------------------------------------------------
1 | use crate::internal::structs::{ErroredPackage, Repo};
2 | use crate::internal::AppExitCode;
3 | use crate::{crash, info, log, repository};
4 |
5 | pub fn build(packages: &[String], exclude: Vec, no_regen: bool, verbose: bool) {
6 | // Read config struct from mlc.toml
7 | let config = crate::internal::parse_cfg(verbose);
8 | log!(verbose, "Config: {:?}", config);
9 | // Check if any packages were passed, if not, imply all
10 | let all = packages.is_empty();
11 | log!(verbose, "All: {:?}", all);
12 |
13 | // Read signing
14 | let signing = config.mode.repository.as_ref().unwrap().signing.enabled;
15 |
16 | // Read on_gen
17 | let on_gen = config.mode.repository.as_ref().unwrap().signing.on_gen;
18 |
19 | // Parse whether to sign on build or not
20 | let sign = if signing && on_gen.is_some() && on_gen.unwrap() {
21 | false
22 | } else {
23 | signing
24 | };
25 | log!(verbose, "Signing: {:?}", sign);
26 |
27 | // Get list of repos and subtract exclude
28 | let mut repos: Vec = config.repositories;
29 | log!(verbose, "{} Repos: {:?}", repos.len(), repos);
30 | if !exclude.is_empty() {
31 | log!(verbose, "Exclude not empty: {:?}", exclude);
32 | for ex in exclude {
33 | repos.retain(|x| *x.name != ex);
34 | }
35 | }
36 |
37 | log!(
38 | verbose,
39 | "Exclusions parsed. Now {} Repos: {:?}",
40 | repos.len(),
41 | repos
42 | );
43 |
44 | // If packages is not empty and all isn't specified, build specified packages
45 | let mut errored: Vec = vec![];
46 | if !packages.is_empty() && !all {
47 | log!(verbose, "Packages not empty: {:?}", packages);
48 | for pkg in packages.iter() {
49 | // If repo is not in config, crash, otherwise, build
50 | if repos.iter().map(|x| x.name.clone()).any(|x| x == *pkg) {
51 | // Otherwise, build
52 | log!(verbose, "Building {}", pkg);
53 |
54 | let code = repository::build(pkg, sign, verbose);
55 | log!(
56 | verbose,
57 | "Package {} finished with exit code: {:?}",
58 | pkg,
59 | code
60 | );
61 |
62 | if code != 0 {
63 | let error = ErroredPackage {
64 | name: pkg.to_string(),
65 | code,
66 | };
67 | errored.push(error);
68 | }
69 | } else {
70 | crash!(
71 | AppExitCode::PkgsNotFound,
72 | "Package repo {} not found in in mlc.toml",
73 | pkg
74 | );
75 | }
76 | }
77 | }
78 |
79 | // If all is specified, attempt to build a package from all repos
80 | if all {
81 | log!(verbose, "Proceeding to build all");
82 |
83 | // Sort by package priority
84 | log!(verbose, "Sorting by priority: {:?}", repos);
85 | repos.sort_by(|a, b| b.priority.cmp(&a.priority));
86 | log!(verbose, "Sorted: {:?}", repos);
87 | for pkg in repos {
88 | log!(verbose, "Building {}", pkg.name);
89 |
90 | let code = repository::build(&pkg.name, sign, verbose);
91 | log!(
92 | verbose,
93 | "Package {} finished with exit code: {:?}",
94 | pkg.name,
95 | code
96 | );
97 |
98 | if code != 0 {
99 | let error = ErroredPackage {
100 | name: pkg.name,
101 | code,
102 | };
103 | errored.push(error);
104 | }
105 | }
106 | }
107 |
108 | // If all is not specified, but packages is empty, crash
109 | if !all && packages.is_empty() {
110 | log!(verbose, "Packages empty. Crashing");
111 | crash!(AppExitCode::PkgsNotFound, "No packages specified");
112 | }
113 |
114 | // If no_regen is passed, do not generate a repository
115 | if !no_regen {
116 | log!(verbose, "Generating repository");
117 | repository::generate(verbose);
118 | }
119 |
120 | // Map errored packages to a string for display
121 | let error_strings: Vec = errored
122 | .iter()
123 | .map(|x| format!("{}: Returned {}", x.name, x.code))
124 | .collect();
125 |
126 | // If errored is not empty, let the user know which packages failed
127 | if !errored.is_empty() {
128 | log!(verbose, "Errored packages: \n{:?}", error_strings);
129 | info!(
130 | "The following packages build jobs returned a non-zero exit code: \n {}",
131 | error_strings.join("\n ")
132 | );
133 | info!("Please check `man 8 makepkg` for more information");
134 | // Check if code 63 appeared at all
135 | if errored.iter().any(|x| x.code == 63) {
136 | log!(verbose, "Code 63 found");
137 | info!("Note: Code 63 is an internal Malachite exit code, and specifies that no PKGBUILD was found.");
138 | }
139 | }
140 | }
141 |
--------------------------------------------------------------------------------
/src/repository/repo.rs:
--------------------------------------------------------------------------------
1 | use std::path::Path;
2 | use std::process::Command;
3 | use std::{env, fs};
4 |
5 | use crate::{crash, info, internal::parse_cfg, internal::AppExitCode, log};
6 |
7 | pub fn generate(verbose: bool) {
8 | // Read config struct from mlc.toml
9 | let config = parse_cfg(verbose);
10 | log!(verbose, "Config: {:?}", config);
11 |
12 | // Get signing from config
13 | let signing = &config.mode.repository.as_ref().unwrap().signing.enabled;
14 | log!(verbose, "Signing: {:?}", signing);
15 |
16 | // Get repository name from config
17 | let name = &config.mode.repository.as_ref().unwrap().name;
18 | log!(verbose, "Name: {}", name);
19 |
20 | // Read on_gen from config
21 | let on_gen = &config.mode.repository.as_ref().unwrap().signing.on_gen;
22 | log!(verbose, "On gen: {:?}", on_gen);
23 |
24 | // Read key from config
25 | let key = &config.mode.repository.as_ref().unwrap().signing.key;
26 | log!(verbose, "Key: {:?}", key);
27 |
28 | info!("Generating repository: {}", name);
29 |
30 | // If repository exists, delete it
31 | if Path::exists(name.as_ref()) {
32 | log!(verbose, "Deleting {}", name);
33 | fs::remove_dir_all(&name).unwrap();
34 | }
35 |
36 | // Create or recreate repository directory
37 | fs::create_dir_all(&name).unwrap();
38 | log!(verbose, "Created {}", name);
39 |
40 | // Copy out packages to repository directory
41 | Command::new("bash")
42 | .args(&["-c", &format!("cp -v out/* {}/", &name)])
43 | .spawn()
44 | .unwrap()
45 | .wait()
46 | .unwrap();
47 | log!(verbose, "Copied out packages to {}", name);
48 |
49 | // Enter repository directory
50 | env::set_current_dir(&name).unwrap();
51 | log!(verbose, "Current dir: {:?}", env::current_dir().unwrap());
52 |
53 | // Sign all package files in repository if signing and on_gen are true
54 | if *signing && on_gen.is_some() && on_gen.unwrap() {
55 | // Get a list of all .tar.* files in repository
56 | let files = fs::read_dir(".").unwrap();
57 |
58 | for file in files {
59 | // Get file name
60 | let file = file.unwrap();
61 | let path = file.path();
62 |
63 | let sign_command = if key.is_some() && !key.as_ref().unwrap().is_empty() {
64 | format!(
65 | "gpg --default-key {} --detach-sign {}",
66 | key.as_ref().unwrap(),
67 | path.to_str().unwrap()
68 | )
69 | } else {
70 | format!("gpg --detach-sign {}", path.to_str().unwrap())
71 | };
72 |
73 | // If extension is either .zst or .xz, sign it
74 | if path.extension().unwrap() == "zst" || path.extension().unwrap() == "xz" {
75 | log!(verbose, "Signing {}", path.display());
76 | Command::new("bash")
77 | .arg("-c")
78 | .args(&[&sign_command])
79 | .spawn()
80 | .unwrap()
81 | .wait()
82 | .unwrap();
83 | }
84 | }
85 | log!(verbose, "Signed repository");
86 | }
87 |
88 | let db = format!("{}.db", &name);
89 | let files = format!("{}.files", &name);
90 |
91 | // Check if package files end with .tar.zst or .tar.xz
92 | let zst = Command::new("bash")
93 | .args(&["-c", "ls *.tar.zst"])
94 | .spawn()
95 | .unwrap()
96 | .wait()
97 | .unwrap();
98 | let xz = Command::new("bash")
99 | .args(&["-c", "ls *.tar.xz"])
100 | .spawn()
101 | .unwrap()
102 | .wait()
103 | .unwrap();
104 |
105 | // This should never happen, crash and burn if it does
106 | if zst.success() && xz.success() {
107 | crash!(
108 | AppExitCode::RepoParseError,
109 | "Both .tar.zst and .tar.xz files found in repository. You've done something wrong. Aborting"
110 | );
111 | }
112 |
113 | // Ensuring aarch64/ALARM support for the future
114 | let aarch64_mode = if zst.success() {
115 | false
116 | } else if xz.success() {
117 | true
118 | } else {
119 | crash!(
120 | AppExitCode::PkgsNotFound,
121 | "No .zst or .xz packages found in repository"
122 | );
123 | // This should theoretically never be reached, but let's just give the compiler what it wants
124 | false
125 | };
126 | let suffix = if aarch64_mode { "xz" } else { "zst" };
127 |
128 | // Create repo.db and repo.files using repo-add
129 | Command::new("bash")
130 | .args(&[
131 | "-c",
132 | &format!(
133 | "GLOBIGNORE=\"*.sig\" repo-add {}.tar.gz *.pkg.tar.{}",
134 | db, suffix
135 | ),
136 | ])
137 | .spawn()
138 | .unwrap()
139 | .wait()
140 | .unwrap();
141 | log!(verbose, "Created {} and {}", db, files);
142 |
143 | // Replace repo.{db,files}.tar.gz with just repo.{db,files}
144 | Command::new("bash")
145 | .args(&[
146 | "-c",
147 | &format!("mv {}.tar.gz {}; mv {}.tar.gz {}", db, db, files, files),
148 | ])
149 | .spawn()
150 | .unwrap()
151 | .wait()
152 | .unwrap();
153 | log!(
154 | verbose,
155 | "Renamed {}.tar.gz to {} and {}.tar.gz to {}",
156 | db,
157 | db,
158 | files,
159 | files
160 | );
161 | }
162 |
--------------------------------------------------------------------------------
/src/operations/clone.rs:
--------------------------------------------------------------------------------
1 | use std::env;
2 | use std::process::Command;
3 |
4 | use crate::{info, log};
5 |
6 | pub fn clone(verbose: bool) {
7 | // Read config struct from mlc.toml
8 | let config = crate::internal::parse_cfg(verbose);
9 | log!(verbose, "Config: {:?}", config);
10 | // Parse repositories from config
11 | let repos = &config.repositories;
12 | log!(verbose, "Repos: {:?}", repos);
13 |
14 | // Get a vector of all files/dirs in the current directory, excluding config file
15 | let dir_paths = std::fs::read_dir(".").unwrap();
16 | let mut dirs = dir_paths
17 | .map(|x| x.unwrap().path().display().to_string())
18 | .collect::>();
19 |
20 | // Remove mlc.toml and .git from output
21 | dirs.retain(|x| *x != "./mlc.toml" && *x != ".\\mlc.toml");
22 | dirs.retain(|x| *x != "./.git" && *x != ".\\.git");
23 |
24 | // If mode is repository, also exclude repository mode directories
25 | if config.mode.repository.is_some() {
26 | dirs.retain(|x| {
27 | *x != format!("./{}", config.mode.repository.as_ref().unwrap().name)
28 | && *x != format!(".\\{}", config.mode.repository.as_ref().unwrap().name)
29 | });
30 | dirs.retain(|x| *x != "./out" && *x != ".\\out");
31 | }
32 | log!(verbose, "Paths with mlc.toml excluded: {:?}", dirs);
33 |
34 | // Creates a vector of the difference between cloned repos and repos defined in config
35 | let mut repo_diff = vec![];
36 | for repo in repos {
37 | let name = &repo.name;
38 |
39 | if !dirs.contains(&format!("./{}", name)) && !dirs.contains(&format!(".\\{}", name)) {
40 | repo_diff.push(repo);
41 | }
42 | }
43 |
44 | // Diff logic
45 | if repo_diff.is_empty() {
46 | // No diff, do nothing
47 | log!(verbose, "No diff");
48 | info!("All repos are already cloned");
49 | } else {
50 | log!(verbose, "Diff: {:?}", repo_diff);
51 | // This is just for pretty display purposes
52 | let display = repo_diff
53 | .iter()
54 | .map(|x| x.name.to_string())
55 | .collect::>()
56 | .join(" ");
57 | info!("New/missing repos to clone: {}", display);
58 |
59 | // Clone all diff repos
60 | for r in repo_diff {
61 | log!(verbose, "Depth: {:?}", r.extra);
62 | log!(verbose, "Cloning {}", r.name);
63 |
64 | if r.extra.is_some() && config.base.mode == "workspace" {
65 | info!(
66 | "Cloning ({} mode): {} with `--depth {}`",
67 | config.base.mode,
68 | r.name,
69 | r.extra.as_ref().unwrap()
70 | );
71 | } else if r.extra.is_some() && config.base.mode == "repository" {
72 | info!(
73 | "Cloning ({} mode): {} at {}",
74 | config.base.mode,
75 | r.name,
76 | r.extra.as_ref().unwrap()
77 | );
78 | } else {
79 | info!("Cloning ({} mode): {}", config.base.mode, r.name);
80 | }
81 |
82 | if r.extra.is_some() && config.base.mode == "workspace" {
83 | // Clone with specified extra depth
84 | Command::new("git")
85 | .args(&["clone", &r.url, &r.name])
86 | // If a branch is specified, clone that specific branch
87 | .args(if r.branch.is_some() {
88 | vec!["-b", r.branch.as_ref().unwrap()]
89 | } else {
90 | vec![]
91 | })
92 | .args(if r.extra.is_some() {
93 | vec!["--depth", r.extra.as_ref().unwrap()]
94 | } else {
95 | vec![]
96 | })
97 | .spawn()
98 | .unwrap()
99 | .wait()
100 | .unwrap();
101 | } else if config.base.mode == "repository" {
102 | // Clone and checkout specified hash
103 | // Create an empty directory with repo.name and enter it
104 | let root_dir = env::current_dir().unwrap();
105 |
106 | // Git clone the repo
107 | Command::new("git")
108 | .args(&["clone", &r.url, &r.name])
109 | .args(if r.branch.is_some() {
110 | vec!["-b", r.branch.as_ref().unwrap()]
111 | } else {
112 | vec![]
113 | })
114 | .spawn()
115 | .unwrap()
116 | .wait()
117 | .unwrap();
118 |
119 | std::env::set_current_dir(&r.name).unwrap();
120 | log!(verbose, "Entered directory: {}", r.name);
121 |
122 | // Git checkout the PKGBUILD from the hash
123 | if r.extra.is_some() {
124 | Command::new("git")
125 | .args(&["checkout", r.extra.as_ref().unwrap()])
126 | .spawn()
127 | .unwrap()
128 | .wait()
129 | .unwrap();
130 | }
131 |
132 | // Return to the root directory
133 | std::env::set_current_dir(root_dir).unwrap();
134 | log!(verbose, "Returned to root directory");
135 | } else {
136 | // Clone normally
137 | Command::new("git")
138 | .args(&["clone", &r.url, &r.name])
139 | .args(if r.branch.is_some() {
140 | vec!["-b", r.branch.as_ref().unwrap()]
141 | } else {
142 | vec![]
143 | })
144 | .spawn()
145 | .unwrap()
146 | .wait()
147 | .unwrap();
148 | }
149 | }
150 | }
151 | }
152 |
--------------------------------------------------------------------------------
/src/operations/pull.rs:
--------------------------------------------------------------------------------
1 | use std::env;
2 | use std::process::Command;
3 |
4 | use crate::info;
5 | use crate::{crash, internal::AppExitCode, log, prompt};
6 |
7 | struct PullParams {
8 | smart_pull: bool,
9 | build_on_update: bool,
10 | no_regen: bool,
11 | }
12 |
13 | fn do_the_pulling(repos: Vec, verbose: bool, params: &PullParams, interactive: bool) {
14 | for repo in repos {
15 | // Set root dir to return after each git pull
16 | let root_dir = env::current_dir().unwrap();
17 | log!(verbose, "Root dir: {:?}", root_dir);
18 |
19 | // Enter repo dir
20 | info!("Entering working directory: {}", &repo);
21 | env::set_current_dir(&repo).unwrap();
22 | log!(verbose, "Current dir: {:?}", env::current_dir().unwrap());
23 |
24 | let mut packages_to_rebuild: Vec = vec![];
25 |
26 | // Pull logic
27 | log!(verbose, "Pulling");
28 | if params.smart_pull {
29 | // Update the remote
30 | log!(verbose, "Smart pull");
31 | Command::new("git")
32 | .args(&["remote", "update"])
33 | .spawn()
34 | .unwrap()
35 | .wait()
36 | .unwrap();
37 |
38 | // Check the repository status
39 | let output = Command::new("git").arg("status").output().unwrap();
40 |
41 | // If there are changes, pull normally
42 | if String::from_utf8(output.stdout)
43 | .unwrap()
44 | .to_string()
45 | .contains("Your branch is behind")
46 | {
47 | info!("Branch out of date, pulling changes");
48 | Command::new("git")
49 | .arg("pull")
50 | .spawn()
51 | .unwrap()
52 | .wait()
53 | .unwrap();
54 |
55 | // If build_on_update is set, rebuild package
56 | if params.build_on_update {
57 | if interactive {
58 | let cont = prompt!(default true, "Rebuild package {}?", &repo);
59 | if cont {
60 | info!("Package {} updated, staging for rebuild", &repo);
61 | log!(verbose, "Pushing package {} to be rebuilt", &repo);
62 | packages_to_rebuild.push(repo);
63 | } else {
64 | info!("Not rebuilding package {}", &repo);
65 | }
66 | } else {
67 | packages_to_rebuild.push(repo);
68 | }
69 | }
70 | } else {
71 | // If there are no changes, alert the user
72 | info!("No changes to pull");
73 | }
74 | } else {
75 | // Pull normally
76 | log!(verbose, "Normal pull");
77 | Command::new("git")
78 | .arg("pull")
79 | .spawn()
80 | .unwrap()
81 | .wait()
82 | .unwrap();
83 | }
84 | // Return to root dir
85 | env::set_current_dir(&root_dir).unwrap();
86 | log!(
87 | verbose,
88 | "Returned to root dir: {:?}",
89 | env::current_dir().unwrap()
90 | );
91 |
92 | // Rebuild packages if necessary
93 | if !packages_to_rebuild.is_empty() && params.build_on_update {
94 | info!("Rebuilding packages: {}", &packages_to_rebuild.join(", "));
95 | log!(verbose, "Rebuilding packages: {:?}", &packages_to_rebuild);
96 |
97 | // Push to build
98 | crate::operations::build(&packages_to_rebuild, vec![], params.no_regen, verbose);
99 |
100 | // Ensure you are in root dir
101 | env::set_current_dir(root_dir).unwrap();
102 | log!(
103 | verbose,
104 | "Returned to root dir: {:?}",
105 | env::current_dir().unwrap()
106 | );
107 | }
108 | }
109 | }
110 |
111 | pub fn pull(
112 | packages: Vec,
113 | exclude: &[String],
114 | verbose: bool,
115 | no_regen: bool,
116 | interactive: bool,
117 | ) {
118 | // Read config file
119 | let config = crate::parse_cfg(verbose);
120 | log!(verbose, "Config: {:?}", config);
121 |
122 | // If no packages are specified, imply all
123 | let all = packages.is_empty();
124 | log!(verbose, "All: {}", all);
125 |
126 | // Read smart_pull from config
127 | let smart_pull = config.base.smart_pull;
128 | log!(verbose, "Smart pull: {}", smart_pull);
129 |
130 | // Read build_on_update from config
131 | let build_on_update = if config.mode.repository.is_some() {
132 | config.mode.repository.unwrap().build_on_update
133 | } else {
134 | false
135 | };
136 | log!(verbose, "Build on update: {}", build_on_update);
137 |
138 | // Read repos from config
139 | let repos = config
140 | .repositories
141 | .iter()
142 | .map(|x| x.name.clone())
143 | .collect::>();
144 | log!(verbose, "Repos: {:?}", repos);
145 |
146 | // Set repos_applicable for next function
147 | let mut repos_applicable = if all { repos } else { packages };
148 | log!(verbose, "Repos applicable: {:?}", repos_applicable);
149 |
150 | // Subtract exclude from repos_applicable
151 | if !exclude.is_empty() {
152 | for ex in exclude.iter() {
153 | repos_applicable.retain(|x| *x != *ex);
154 | }
155 | }
156 | log!(verbose, "Exclude: {:?}", exclude);
157 | log!(verbose, "Repos applicable excluded: {:?}", repos_applicable);
158 |
159 | // If all is not specified and packages is empty, crash
160 | if repos_applicable.is_empty() {
161 | crash!(AppExitCode::PkgsNotFound, "No packages specified");
162 | }
163 |
164 | // Sort repos_applicable by priority
165 | repos_applicable.sort_by(|a, b| {
166 | config
167 | .repositories
168 | .iter()
169 | .find(|x| x.name == *a)
170 | .unwrap()
171 | .priority
172 | .cmp(
173 | &config
174 | .repositories
175 | .iter()
176 | .find(|x| x.name == *b)
177 | .unwrap()
178 | .priority,
179 | )
180 | });
181 |
182 | log!(verbose, "Pulling {:?}", repos_applicable);
183 |
184 | // If any repos are not in the config, run a clone
185 | for repo in &repos_applicable {
186 | if !std::path::Path::new(repo).exists() {
187 | info!(
188 | "Repo {} does not exist, ensuring all repos are cloned",
189 | repo
190 | );
191 | crate::operations::clone(verbose);
192 | break;
193 | }
194 | }
195 |
196 | // Pull!
197 | do_the_pulling(
198 | repos_applicable,
199 | verbose,
200 | &PullParams {
201 | smart_pull,
202 | build_on_update,
203 | no_regen,
204 | },
205 | interactive,
206 | );
207 | }
208 |
--------------------------------------------------------------------------------
/src/operations/info.rs:
--------------------------------------------------------------------------------
1 | use colored::Colorize;
2 | use spinoff::{Color, Spinner, Spinners};
3 | use std::env;
4 | use std::fmt::Write;
5 | use std::path::Path;
6 | use std::process::Command;
7 | use tabled::Tabled;
8 |
9 | use crate::{crash, info, internal::AppExitCode, log};
10 |
11 | // For displaying the table of contents
12 | #[derive(Clone, tabled::Tabled, Debug)]
13 | struct RepoDisplayGit {
14 | #[tabled(rename = "Name")]
15 | name: String,
16 | #[tabled(rename = "URL")]
17 | url: String,
18 | #[tabled(skip)]
19 | priority: usize,
20 | #[tabled(rename = "Git Info")]
21 | git_info: String,
22 | }
23 |
24 | #[derive(Clone, tabled::Tabled, Debug)]
25 | struct RepoDisplay {
26 | #[tabled(rename = "Name")]
27 | name: String,
28 | #[tabled(rename = "URL")]
29 | url: String,
30 | #[tabled(skip)]
31 | priority: usize,
32 | }
33 |
34 | pub fn git_status(verbose: bool, repo: &str, colorblind: bool) -> String {
35 | let dir = env::current_dir().unwrap();
36 | log!(
37 | verbose,
38 | "Current directory: {}",
39 | env::current_dir().unwrap().display()
40 | );
41 | env::set_current_dir(&repo).unwrap_or_else(|e| {
42 | crash!(
43 | AppExitCode::RepoParseError,
44 | "Failed to enter directory {} for Git info: {}, Have you initialized the repo?",
45 | repo,
46 | e.to_string()
47 | );
48 | });
49 | log!(verbose, "Current directory: {}", repo);
50 |
51 | let output = Command::new("git").arg("status").output().unwrap();
52 | let output = String::from_utf8(output.stdout).unwrap();
53 | log!(verbose, "Git status: {}", output);
54 |
55 | let unstaged = output.contains("Changes not staged for commit")
56 | || output.contains("Changes to be committed");
57 | let untracked = output.contains("Untracked files");
58 | let dirty = unstaged || untracked;
59 |
60 | let pull = output.contains("Your branch is behind");
61 | let push = output.contains("Your branch is ahead");
62 |
63 | let latest_commit = Command::new("git")
64 | .args(&["log", "--pretty=%h", "-1"])
65 | .output()
66 | .unwrap();
67 | let mut latest_commit = String::from_utf8(latest_commit.stdout).unwrap();
68 | latest_commit.retain(|c| !c.is_whitespace());
69 |
70 | let output = if colorblind {
71 | format!(
72 | "{} {} {} {}",
73 | if dirty { "D".red() } else { "D".bright_blue() },
74 | if pull { "Pl".red() } else { "Pl".bright_blue() },
75 | if push { "Ps".red() } else { "Ps".bright_blue() },
76 | latest_commit
77 | )
78 | } else {
79 | format!(
80 | "{} {} {} {}",
81 | if dirty { "D".red() } else { "D".green() },
82 | if pull { "Pl".red() } else { "Pl".green() },
83 | if push { "Ps".red() } else { "Ps".green() },
84 | latest_commit
85 | )
86 | };
87 | env::set_current_dir(&dir).unwrap();
88 | log!(verbose, "Current directory: {}", dir.display());
89 | output
90 | }
91 |
92 | pub fn info(verbose: bool) {
93 | log!(verbose, "Showing Info");
94 | // Parse config from mlc.toml
95 | let config = crate::internal::parse_cfg(verbose);
96 | log!(verbose, "Config: {:?}", config);
97 |
98 | // Check for git_info
99 | let git_info = if config.mode.workspace.is_some() {
100 | config.mode.workspace.as_ref().unwrap().git_info
101 | } else {
102 | false
103 | };
104 | log!(verbose, "Git info: {}", git_info);
105 |
106 | // Check for colorblind mode
107 | let colorblind = if config.mode.workspace.is_some() {
108 | config.mode.workspace.as_ref().unwrap().colorblind
109 | } else {
110 | false
111 | };
112 | log!(verbose, "Colorblind: {}", colorblind);
113 |
114 | // Add the branch to the name if it's not the default branch for said repository
115 | let repos_unparsed = config.repositories;
116 | let mut repos = vec![];
117 | let mut repos_git = vec![];
118 |
119 | if git_info {
120 | // Crash early if directories are not found for git_info
121 | for repo in &repos_unparsed {
122 | if !Path::new(&repo.name).exists() {
123 | crash!(
124 | AppExitCode::RepoParseError,
125 | "Failed to check directory {} for Git info, have you initialized the repo?",
126 | repo.name,
127 | );
128 | };
129 | }
130 |
131 | // Start the spinner
132 | let sp = Spinner::new(
133 | Spinners::Line,
134 | format!("{}", "Parsing Git Info...".bold()),
135 | Color::Green,
136 | );
137 |
138 | // Construct bash script to run git remote upgrade on all repos asynchronously
139 | // This helps speed up the operation when, for example, you have a lot of repositories and you store your SSH key as a subkey of your GPG key on a yubikey
140 | // This took my `mlc info` time down from 17s to 8s (i have the above described setup)
141 | let mut bash_script = String::new();
142 | bash_script.push_str(
143 | "\n\
144 | #!/usr/bin/env bash\n\
145 | \n\
146 | # This script will run `git remote update` in all repositories\n\
147 | pull() { cd $1; git remote update; cd -; }\n\
148 | \n",
149 | );
150 | for repo in &repos_unparsed {
151 | writeln!(bash_script, "pull {} &", repo.name).unwrap();
152 | }
153 | bash_script.push_str("wait\n");
154 |
155 | log!(verbose, "Bash script: {}", bash_script);
156 |
157 | // Run the bash script
158 | Command::new("bash")
159 | .arg("-c")
160 | .arg(bash_script)
161 | .output()
162 | .unwrap();
163 |
164 | // Stop the spinner with a success message
165 | let text = format!("{}", "Parsing Git Info... Done".bold());
166 | let symbol = format!("{}", "✔".bold().green());
167 |
168 | sp.stop_and_persist(&symbol, &text);
169 | log!(verbose, "Repos: {:?}", repos);
170 | }
171 |
172 | // Iterate over all repositories
173 | for repo in repos_unparsed {
174 | // Get name with branch, '/' serving as the delimiter
175 | let name = if repo.branch.is_some() {
176 | format!("{}/{}", repo.name, repo.branch.unwrap())
177 | } else {
178 | repo.name.clone()
179 | };
180 |
181 | // Get git info, if applicable
182 | let git_info_string = if git_info {
183 | let info = Some(git_status(
184 | verbose,
185 | &repo.name,
186 | config.mode.workspace.as_ref().unwrap().colorblind,
187 | ));
188 | info
189 | } else {
190 | None
191 | };
192 |
193 | // Push to the correct vector, we're using a separate vector for git info because
194 | // the struct we're displaying is different
195 | if git_info {
196 | repos_git.push(RepoDisplayGit {
197 | name,
198 | url: repo.url.clone(),
199 | priority: repo.priority,
200 | git_info: git_info_string.unwrap(),
201 | });
202 | } else {
203 | repos.push(RepoDisplay {
204 | name,
205 | url: repo.url.clone(),
206 | priority: repo.priority,
207 | });
208 | }
209 | }
210 |
211 | // Sort by priority
212 | repos.sort_by(|a, b| b.priority.cmp(&a.priority));
213 | repos_git.sort_by(|a, b| b.priority.cmp(&a.priority));
214 | if git_info {
215 | log!(verbose, "Repos Sorted: {:?}", repos_git);
216 | } else {
217 | log!(verbose, "Repos Sorted: {:?}", repos);
218 | }
219 |
220 | // Displaying basic info about the Malachite Repository
221 | let internal_name = if config.mode.repository.is_none()
222 | || config.mode.repository.as_ref().unwrap().name.is_empty()
223 | {
224 | env::current_dir()
225 | .unwrap()
226 | .file_name()
227 | .unwrap()
228 | .to_str()
229 | .unwrap()
230 | .to_string()
231 | } else {
232 | config.mode.repository.unwrap().name
233 | };
234 | let name = format!(
235 | "{} \"{}\":",
236 | // Sidenote: It should NOT be this convoluted to capitalise the first character of a string in rust. What the fuck.
237 | String::from_utf8_lossy(&[config.base.mode.as_bytes()[0].to_ascii_uppercase()])
238 | + &config.base.mode[1..],
239 | internal_name
240 | );
241 |
242 | // Get terminal width for table formatting
243 | let width = match crossterm::terminal::size() {
244 | Ok((w, _)) => w,
245 | Err(_) => 80,
246 | };
247 |
248 | // Create table for displaying info
249 | let table = if git_info {
250 | tabled::Table::new(&repos_git)
251 | .with(tabled::Style::modern())
252 | .with(tabled::Width::wrap(width as usize).keep_words())
253 | .to_string()
254 | } else {
255 | tabled::Table::new(&repos)
256 | .with(tabled::Style::modern())
257 | .with(tabled::Width::wrap(width as usize).keep_words())
258 | .to_string()
259 | };
260 |
261 | // Get length of Vec for displaying in the table
262 | let len = if git_info {
263 | repos_git.len()
264 | } else {
265 | repos.len()
266 | };
267 |
268 | // Print all of the info
269 | info!("{}", name);
270 | info!("Total Repositories: {}", len.to_string().green());
271 | println!("{}", table);
272 | if config.mode.workspace.is_some() && config.mode.workspace.as_ref().unwrap().git_info {
273 | info!(
274 | "D: Dirty - Unstaged Changes \n \
275 | Pl: Pull - Changes at Remote \n \
276 | Ps: Push - Unpushed Changes \n \
277 | {}: Dirty, {}: Clean",
278 | " ".on_red(),
279 | if config.mode.workspace.unwrap().colorblind {
280 | " ".on_bright_blue()
281 | } else {
282 | " ".on_green()
283 | }
284 | );
285 | }
286 | }
287 |
--------------------------------------------------------------------------------
/Cargo.lock:
--------------------------------------------------------------------------------
1 | # This file is automatically @generated by Cargo.
2 | # It is not intended for manual editing.
3 | version = 3
4 |
5 | [[package]]
6 | name = "Malachite"
7 | version = "2.1.0"
8 | dependencies = [
9 | "clap",
10 | "colored",
11 | "crossterm",
12 | "libc",
13 | "mimalloc",
14 | "regex",
15 | "rm_rf",
16 | "serde",
17 | "serde_derive",
18 | "spinoff",
19 | "tabled",
20 | "toml",
21 | ]
22 |
23 | [[package]]
24 | name = "ansi-str"
25 | version = "0.3.0"
26 | source = "registry+https://github.com/rust-lang/crates.io-index"
27 | checksum = "e50acdf02a3ac61856d5c8d576a8b5fb452a6549f667ca29fefaa18c2cd05135"
28 | dependencies = [
29 | "ansitok",
30 | ]
31 |
32 | [[package]]
33 | name = "ansitok"
34 | version = "0.1.0"
35 | source = "registry+https://github.com/rust-lang/crates.io-index"
36 | checksum = "c2c6eb31f539d8fc1df948eb26452d6c781be4c9883663e7acb258644b71d5b1"
37 | dependencies = [
38 | "nom",
39 | ]
40 |
41 | [[package]]
42 | name = "arrayvec"
43 | version = "0.5.2"
44 | source = "registry+https://github.com/rust-lang/crates.io-index"
45 | checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b"
46 |
47 | [[package]]
48 | name = "atty"
49 | version = "0.2.14"
50 | source = "registry+https://github.com/rust-lang/crates.io-index"
51 | checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8"
52 | dependencies = [
53 | "hermit-abi",
54 | "libc",
55 | "winapi",
56 | ]
57 |
58 | [[package]]
59 | name = "autocfg"
60 | version = "1.1.0"
61 | source = "registry+https://github.com/rust-lang/crates.io-index"
62 | checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
63 |
64 | [[package]]
65 | name = "bitflags"
66 | version = "1.3.2"
67 | source = "registry+https://github.com/rust-lang/crates.io-index"
68 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
69 |
70 | [[package]]
71 | name = "bytecount"
72 | version = "0.6.3"
73 | source = "registry+https://github.com/rust-lang/crates.io-index"
74 | checksum = "2c676a478f63e9fa2dd5368a42f28bba0d6c560b775f38583c8bbaa7fcd67c9c"
75 |
76 | [[package]]
77 | name = "cc"
78 | version = "1.0.73"
79 | source = "registry+https://github.com/rust-lang/crates.io-index"
80 | checksum = "2fff2a6927b3bb87f9595d67196a70493f627687a71d87a0d692242c33f58c11"
81 |
82 | [[package]]
83 | name = "cfg-if"
84 | version = "1.0.0"
85 | source = "registry+https://github.com/rust-lang/crates.io-index"
86 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
87 |
88 | [[package]]
89 | name = "clap"
90 | version = "3.2.14"
91 | source = "registry+https://github.com/rust-lang/crates.io-index"
92 | checksum = "54635806b078b7925d6e36810b1755f2a4b5b4d57560432c1ecf60bcbe10602b"
93 | dependencies = [
94 | "atty",
95 | "bitflags",
96 | "clap_derive",
97 | "clap_lex",
98 | "indexmap",
99 | "once_cell",
100 | "strsim",
101 | "termcolor",
102 | "textwrap",
103 | ]
104 |
105 | [[package]]
106 | name = "clap_derive"
107 | version = "3.2.7"
108 | source = "registry+https://github.com/rust-lang/crates.io-index"
109 | checksum = "759bf187376e1afa7b85b959e6a664a3e7a95203415dba952ad19139e798f902"
110 | dependencies = [
111 | "heck",
112 | "proc-macro-error",
113 | "proc-macro2",
114 | "quote",
115 | "syn",
116 | ]
117 |
118 | [[package]]
119 | name = "clap_lex"
120 | version = "0.2.4"
121 | source = "registry+https://github.com/rust-lang/crates.io-index"
122 | checksum = "2850f2f5a82cbf437dd5af4d49848fbdfc27c157c3d010345776f952765261c5"
123 | dependencies = [
124 | "os_str_bytes",
125 | ]
126 |
127 | [[package]]
128 | name = "colored"
129 | version = "2.0.0"
130 | source = "registry+https://github.com/rust-lang/crates.io-index"
131 | checksum = "b3616f750b84d8f0de8a58bda93e08e2a81ad3f523089b05f1dffecab48c6cbd"
132 | dependencies = [
133 | "atty",
134 | "lazy_static",
135 | "winapi",
136 | ]
137 |
138 | [[package]]
139 | name = "crossterm"
140 | version = "0.25.0"
141 | source = "registry+https://github.com/rust-lang/crates.io-index"
142 | checksum = "e64e6c0fbe2c17357405f7c758c1ef960fce08bdfb2c03d88d2a18d7e09c4b67"
143 | dependencies = [
144 | "bitflags",
145 | "crossterm_winapi",
146 | "libc",
147 | "mio",
148 | "parking_lot",
149 | "signal-hook",
150 | "signal-hook-mio",
151 | "winapi",
152 | ]
153 |
154 | [[package]]
155 | name = "crossterm_winapi"
156 | version = "0.9.0"
157 | source = "registry+https://github.com/rust-lang/crates.io-index"
158 | checksum = "2ae1b35a484aa10e07fe0638d02301c5ad24de82d310ccbd2f3693da5f09bf1c"
159 | dependencies = [
160 | "winapi",
161 | ]
162 |
163 | [[package]]
164 | name = "fnv"
165 | version = "1.0.7"
166 | source = "registry+https://github.com/rust-lang/crates.io-index"
167 | checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
168 |
169 | [[package]]
170 | name = "hashbrown"
171 | version = "0.12.3"
172 | source = "registry+https://github.com/rust-lang/crates.io-index"
173 | checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
174 |
175 | [[package]]
176 | name = "heck"
177 | version = "0.4.0"
178 | source = "registry+https://github.com/rust-lang/crates.io-index"
179 | checksum = "2540771e65fc8cb83cd6e8a237f70c319bd5c29f78ed1084ba5d50eeac86f7f9"
180 |
181 | [[package]]
182 | name = "hermit-abi"
183 | version = "0.1.19"
184 | source = "registry+https://github.com/rust-lang/crates.io-index"
185 | checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33"
186 | dependencies = [
187 | "libc",
188 | ]
189 |
190 | [[package]]
191 | name = "indexmap"
192 | version = "1.9.1"
193 | source = "registry+https://github.com/rust-lang/crates.io-index"
194 | checksum = "10a35a97730320ffe8e2d410b5d3b69279b98d2c14bdb8b70ea89ecf7888d41e"
195 | dependencies = [
196 | "autocfg",
197 | "hashbrown",
198 | ]
199 |
200 | [[package]]
201 | name = "lazy_static"
202 | version = "1.4.0"
203 | source = "registry+https://github.com/rust-lang/crates.io-index"
204 | checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
205 |
206 | [[package]]
207 | name = "libc"
208 | version = "0.2.126"
209 | source = "registry+https://github.com/rust-lang/crates.io-index"
210 | checksum = "349d5a591cd28b49e1d1037471617a32ddcda5731b99419008085f72d5a53836"
211 |
212 | [[package]]
213 | name = "libmimalloc-sys"
214 | version = "0.1.25"
215 | source = "registry+https://github.com/rust-lang/crates.io-index"
216 | checksum = "11ca136052550448f55df7898c6dbe651c6b574fe38a0d9ea687a9f8088a2e2c"
217 | dependencies = [
218 | "cc",
219 | ]
220 |
221 | [[package]]
222 | name = "lock_api"
223 | version = "0.4.7"
224 | source = "registry+https://github.com/rust-lang/crates.io-index"
225 | checksum = "327fa5b6a6940e4699ec49a9beae1ea4845c6bab9314e4f84ac68742139d8c53"
226 | dependencies = [
227 | "autocfg",
228 | "scopeguard",
229 | ]
230 |
231 | [[package]]
232 | name = "log"
233 | version = "0.4.17"
234 | source = "registry+https://github.com/rust-lang/crates.io-index"
235 | checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e"
236 | dependencies = [
237 | "cfg-if",
238 | ]
239 |
240 | [[package]]
241 | name = "maplit"
242 | version = "1.0.2"
243 | source = "registry+https://github.com/rust-lang/crates.io-index"
244 | checksum = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d"
245 |
246 | [[package]]
247 | name = "memchr"
248 | version = "2.5.0"
249 | source = "registry+https://github.com/rust-lang/crates.io-index"
250 | checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d"
251 |
252 | [[package]]
253 | name = "mimalloc"
254 | version = "0.1.29"
255 | source = "registry+https://github.com/rust-lang/crates.io-index"
256 | checksum = "2f64ad83c969af2e732e907564deb0d0ed393cec4af80776f77dd77a1a427698"
257 | dependencies = [
258 | "libmimalloc-sys",
259 | ]
260 |
261 | [[package]]
262 | name = "minimal-lexical"
263 | version = "0.2.1"
264 | source = "registry+https://github.com/rust-lang/crates.io-index"
265 | checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
266 |
267 | [[package]]
268 | name = "mio"
269 | version = "0.8.4"
270 | source = "registry+https://github.com/rust-lang/crates.io-index"
271 | checksum = "57ee1c23c7c63b0c9250c339ffdc69255f110b298b901b9f6c82547b7b87caaf"
272 | dependencies = [
273 | "libc",
274 | "log",
275 | "wasi",
276 | "windows-sys",
277 | ]
278 |
279 | [[package]]
280 | name = "nom"
281 | version = "7.1.1"
282 | source = "registry+https://github.com/rust-lang/crates.io-index"
283 | checksum = "a8903e5a29a317527874d0402f867152a3d21c908bb0b933e416c65e301d4c36"
284 | dependencies = [
285 | "memchr",
286 | "minimal-lexical",
287 | ]
288 |
289 | [[package]]
290 | name = "once_cell"
291 | version = "1.13.0"
292 | source = "registry+https://github.com/rust-lang/crates.io-index"
293 | checksum = "18a6dbe30758c9f83eb00cbea4ac95966305f5a7772f3f42ebfc7fc7eddbd8e1"
294 |
295 | [[package]]
296 | name = "os_str_bytes"
297 | version = "6.2.0"
298 | source = "registry+https://github.com/rust-lang/crates.io-index"
299 | checksum = "648001efe5d5c0102d8cea768e348da85d90af8ba91f0bea908f157951493cd4"
300 |
301 | [[package]]
302 | name = "papergrid"
303 | version = "0.5.1"
304 | source = "registry+https://github.com/rust-lang/crates.io-index"
305 | checksum = "453cf71f2a37af495a1a124bf30d4d7469cfbea58e9f2479be9d222396a518a2"
306 | dependencies = [
307 | "ansi-str",
308 | "bytecount",
309 | "fnv",
310 | "strip-ansi-escapes",
311 | "unicode-width",
312 | ]
313 |
314 | [[package]]
315 | name = "parking_lot"
316 | version = "0.12.1"
317 | source = "registry+https://github.com/rust-lang/crates.io-index"
318 | checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f"
319 | dependencies = [
320 | "lock_api",
321 | "parking_lot_core",
322 | ]
323 |
324 | [[package]]
325 | name = "parking_lot_core"
326 | version = "0.9.3"
327 | source = "registry+https://github.com/rust-lang/crates.io-index"
328 | checksum = "09a279cbf25cb0757810394fbc1e359949b59e348145c643a939a525692e6929"
329 | dependencies = [
330 | "cfg-if",
331 | "libc",
332 | "redox_syscall",
333 | "smallvec",
334 | "windows-sys",
335 | ]
336 |
337 | [[package]]
338 | name = "proc-macro-error"
339 | version = "1.0.4"
340 | source = "registry+https://github.com/rust-lang/crates.io-index"
341 | checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c"
342 | dependencies = [
343 | "proc-macro-error-attr",
344 | "proc-macro2",
345 | "quote",
346 | "syn",
347 | "version_check",
348 | ]
349 |
350 | [[package]]
351 | name = "proc-macro-error-attr"
352 | version = "1.0.4"
353 | source = "registry+https://github.com/rust-lang/crates.io-index"
354 | checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869"
355 | dependencies = [
356 | "proc-macro2",
357 | "quote",
358 | "version_check",
359 | ]
360 |
361 | [[package]]
362 | name = "proc-macro2"
363 | version = "1.0.40"
364 | source = "registry+https://github.com/rust-lang/crates.io-index"
365 | checksum = "dd96a1e8ed2596c337f8eae5f24924ec83f5ad5ab21ea8e455d3566c69fbcaf7"
366 | dependencies = [
367 | "unicode-ident",
368 | ]
369 |
370 | [[package]]
371 | name = "psm"
372 | version = "0.1.20"
373 | source = "registry+https://github.com/rust-lang/crates.io-index"
374 | checksum = "f446d0a6efba22928558c4fb4ce0b3fd6c89b0061343e390bf01a703742b8125"
375 | dependencies = [
376 | "cc",
377 | ]
378 |
379 | [[package]]
380 | name = "quote"
381 | version = "1.0.20"
382 | source = "registry+https://github.com/rust-lang/crates.io-index"
383 | checksum = "3bcdf212e9776fbcb2d23ab029360416bb1706b1aea2d1a5ba002727cbcab804"
384 | dependencies = [
385 | "proc-macro2",
386 | ]
387 |
388 | [[package]]
389 | name = "redox_syscall"
390 | version = "0.2.15"
391 | source = "registry+https://github.com/rust-lang/crates.io-index"
392 | checksum = "534cfe58d6a18cc17120fbf4635d53d14691c1fe4d951064df9bd326178d7d5a"
393 | dependencies = [
394 | "bitflags",
395 | ]
396 |
397 | [[package]]
398 | name = "regex"
399 | version = "1.6.0"
400 | source = "registry+https://github.com/rust-lang/crates.io-index"
401 | checksum = "4c4eb3267174b8c6c2f654116623910a0fef09c4753f8dd83db29c48a0df988b"
402 | dependencies = [
403 | "regex-syntax",
404 | ]
405 |
406 | [[package]]
407 | name = "regex-syntax"
408 | version = "0.6.27"
409 | source = "registry+https://github.com/rust-lang/crates.io-index"
410 | checksum = "a3f87b73ce11b1619a3c6332f45341e0047173771e8b8b73f87bfeefb7b56244"
411 |
412 | [[package]]
413 | name = "rm_rf"
414 | version = "0.6.2"
415 | source = "registry+https://github.com/rust-lang/crates.io-index"
416 | checksum = "3443b7a35aa12ed2e99edfc0ecbefe6a53b4848305cc83e29981dfa1aea1f71e"
417 | dependencies = [
418 | "stacker",
419 | ]
420 |
421 | [[package]]
422 | name = "rustversion"
423 | version = "1.0.8"
424 | source = "registry+https://github.com/rust-lang/crates.io-index"
425 | checksum = "24c8ad4f0c00e1eb5bc7614d236a7f1300e3dbd76b68cac8e06fb00b015ad8d8"
426 |
427 | [[package]]
428 | name = "scopeguard"
429 | version = "1.1.0"
430 | source = "registry+https://github.com/rust-lang/crates.io-index"
431 | checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd"
432 |
433 | [[package]]
434 | name = "serde"
435 | version = "1.0.140"
436 | source = "registry+https://github.com/rust-lang/crates.io-index"
437 | checksum = "fc855a42c7967b7c369eb5860f7164ef1f6f81c20c7cc1141f2a604e18723b03"
438 |
439 | [[package]]
440 | name = "serde_derive"
441 | version = "1.0.140"
442 | source = "registry+https://github.com/rust-lang/crates.io-index"
443 | checksum = "6f2122636b9fe3b81f1cb25099fcf2d3f542cdb1d45940d56c713158884a05da"
444 | dependencies = [
445 | "proc-macro2",
446 | "quote",
447 | "syn",
448 | ]
449 |
450 | [[package]]
451 | name = "signal-hook"
452 | version = "0.3.14"
453 | source = "registry+https://github.com/rust-lang/crates.io-index"
454 | checksum = "a253b5e89e2698464fc26b545c9edceb338e18a89effeeecfea192c3025be29d"
455 | dependencies = [
456 | "libc",
457 | "signal-hook-registry",
458 | ]
459 |
460 | [[package]]
461 | name = "signal-hook-mio"
462 | version = "0.2.3"
463 | source = "registry+https://github.com/rust-lang/crates.io-index"
464 | checksum = "29ad2e15f37ec9a6cc544097b78a1ec90001e9f71b81338ca39f430adaca99af"
465 | dependencies = [
466 | "libc",
467 | "mio",
468 | "signal-hook",
469 | ]
470 |
471 | [[package]]
472 | name = "signal-hook-registry"
473 | version = "1.4.0"
474 | source = "registry+https://github.com/rust-lang/crates.io-index"
475 | checksum = "e51e73328dc4ac0c7ccbda3a494dfa03df1de2f46018127f60c693f2648455b0"
476 | dependencies = [
477 | "libc",
478 | ]
479 |
480 | [[package]]
481 | name = "smallvec"
482 | version = "1.9.0"
483 | source = "registry+https://github.com/rust-lang/crates.io-index"
484 | checksum = "2fd0db749597d91ff862fd1d55ea87f7855a744a8425a64695b6fca237d1dad1"
485 |
486 | [[package]]
487 | name = "spinoff"
488 | version = "0.5.4"
489 | source = "registry+https://github.com/rust-lang/crates.io-index"
490 | checksum = "812db6f40551bdcdb10e1d2070ec33f69805d2bfb7e59426c7d14e7e1b4194dd"
491 | dependencies = [
492 | "colored",
493 | "maplit",
494 | "once_cell",
495 | "strum",
496 | ]
497 |
498 | [[package]]
499 | name = "stacker"
500 | version = "0.1.15"
501 | source = "registry+https://github.com/rust-lang/crates.io-index"
502 | checksum = "c886bd4480155fd3ef527d45e9ac8dd7118a898a46530b7b94c3e21866259fce"
503 | dependencies = [
504 | "cc",
505 | "cfg-if",
506 | "libc",
507 | "psm",
508 | "winapi",
509 | ]
510 |
511 | [[package]]
512 | name = "strip-ansi-escapes"
513 | version = "0.1.1"
514 | source = "registry+https://github.com/rust-lang/crates.io-index"
515 | checksum = "011cbb39cf7c1f62871aea3cc46e5817b0937b49e9447370c93cacbe93a766d8"
516 | dependencies = [
517 | "vte",
518 | ]
519 |
520 | [[package]]
521 | name = "strsim"
522 | version = "0.10.0"
523 | source = "registry+https://github.com/rust-lang/crates.io-index"
524 | checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
525 |
526 | [[package]]
527 | name = "strum"
528 | version = "0.24.1"
529 | source = "registry+https://github.com/rust-lang/crates.io-index"
530 | checksum = "063e6045c0e62079840579a7e47a355ae92f60eb74daaf156fb1e84ba164e63f"
531 | dependencies = [
532 | "strum_macros",
533 | ]
534 |
535 | [[package]]
536 | name = "strum_macros"
537 | version = "0.24.2"
538 | source = "registry+https://github.com/rust-lang/crates.io-index"
539 | checksum = "4faebde00e8ff94316c01800f9054fd2ba77d30d9e922541913051d1d978918b"
540 | dependencies = [
541 | "heck",
542 | "proc-macro2",
543 | "quote",
544 | "rustversion",
545 | "syn",
546 | ]
547 |
548 | [[package]]
549 | name = "syn"
550 | version = "1.0.98"
551 | source = "registry+https://github.com/rust-lang/crates.io-index"
552 | checksum = "c50aef8a904de4c23c788f104b7dddc7d6f79c647c7c8ce4cc8f73eb0ca773dd"
553 | dependencies = [
554 | "proc-macro2",
555 | "quote",
556 | "unicode-ident",
557 | ]
558 |
559 | [[package]]
560 | name = "tabled"
561 | version = "0.8.0"
562 | source = "registry+https://github.com/rust-lang/crates.io-index"
563 | checksum = "e5b2f8c37d26d87d2252187b0a45ea3cbf42baca10377c7e7eaaa2800fa9bf97"
564 | dependencies = [
565 | "ansi-str",
566 | "papergrid",
567 | "tabled_derive",
568 | "unicode-width",
569 | ]
570 |
571 | [[package]]
572 | name = "tabled_derive"
573 | version = "0.4.0"
574 | source = "registry+https://github.com/rust-lang/crates.io-index"
575 | checksum = "f9ee618502f497abf593e1c5c9577f34775b111480009ffccd7ad70d23fcaba8"
576 | dependencies = [
577 | "heck",
578 | "proc-macro-error",
579 | "proc-macro2",
580 | "quote",
581 | "syn",
582 | ]
583 |
584 | [[package]]
585 | name = "termcolor"
586 | version = "1.1.3"
587 | source = "registry+https://github.com/rust-lang/crates.io-index"
588 | checksum = "bab24d30b911b2376f3a13cc2cd443142f0c81dda04c118693e35b3835757755"
589 | dependencies = [
590 | "winapi-util",
591 | ]
592 |
593 | [[package]]
594 | name = "textwrap"
595 | version = "0.15.0"
596 | source = "registry+https://github.com/rust-lang/crates.io-index"
597 | checksum = "b1141d4d61095b28419e22cb0bbf02755f5e54e0526f97f1e3d1d160e60885fb"
598 |
599 | [[package]]
600 | name = "toml"
601 | version = "0.5.9"
602 | source = "registry+https://github.com/rust-lang/crates.io-index"
603 | checksum = "8d82e1a7758622a465f8cee077614c73484dac5b836c02ff6a40d5d1010324d7"
604 | dependencies = [
605 | "serde",
606 | ]
607 |
608 | [[package]]
609 | name = "unicode-ident"
610 | version = "1.0.2"
611 | source = "registry+https://github.com/rust-lang/crates.io-index"
612 | checksum = "15c61ba63f9235225a22310255a29b806b907c9b8c964bcbd0a2c70f3f2deea7"
613 |
614 | [[package]]
615 | name = "unicode-width"
616 | version = "0.1.9"
617 | source = "registry+https://github.com/rust-lang/crates.io-index"
618 | checksum = "3ed742d4ea2bd1176e236172c8429aaf54486e7ac098db29ffe6529e0ce50973"
619 |
620 | [[package]]
621 | name = "utf8parse"
622 | version = "0.2.0"
623 | source = "registry+https://github.com/rust-lang/crates.io-index"
624 | checksum = "936e4b492acfd135421d8dca4b1aa80a7bfc26e702ef3af710e0752684df5372"
625 |
626 | [[package]]
627 | name = "version_check"
628 | version = "0.9.4"
629 | source = "registry+https://github.com/rust-lang/crates.io-index"
630 | checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
631 |
632 | [[package]]
633 | name = "vte"
634 | version = "0.10.1"
635 | source = "registry+https://github.com/rust-lang/crates.io-index"
636 | checksum = "6cbce692ab4ca2f1f3047fcf732430249c0e971bfdd2b234cf2c47ad93af5983"
637 | dependencies = [
638 | "arrayvec",
639 | "utf8parse",
640 | "vte_generate_state_changes",
641 | ]
642 |
643 | [[package]]
644 | name = "vte_generate_state_changes"
645 | version = "0.1.1"
646 | source = "registry+https://github.com/rust-lang/crates.io-index"
647 | checksum = "d257817081c7dffcdbab24b9e62d2def62e2ff7d00b1c20062551e6cccc145ff"
648 | dependencies = [
649 | "proc-macro2",
650 | "quote",
651 | ]
652 |
653 | [[package]]
654 | name = "wasi"
655 | version = "0.11.0+wasi-snapshot-preview1"
656 | source = "registry+https://github.com/rust-lang/crates.io-index"
657 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
658 |
659 | [[package]]
660 | name = "winapi"
661 | version = "0.3.9"
662 | source = "registry+https://github.com/rust-lang/crates.io-index"
663 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
664 | dependencies = [
665 | "winapi-i686-pc-windows-gnu",
666 | "winapi-x86_64-pc-windows-gnu",
667 | ]
668 |
669 | [[package]]
670 | name = "winapi-i686-pc-windows-gnu"
671 | version = "0.4.0"
672 | source = "registry+https://github.com/rust-lang/crates.io-index"
673 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
674 |
675 | [[package]]
676 | name = "winapi-util"
677 | version = "0.1.5"
678 | source = "registry+https://github.com/rust-lang/crates.io-index"
679 | checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178"
680 | dependencies = [
681 | "winapi",
682 | ]
683 |
684 | [[package]]
685 | name = "winapi-x86_64-pc-windows-gnu"
686 | version = "0.4.0"
687 | source = "registry+https://github.com/rust-lang/crates.io-index"
688 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
689 |
690 | [[package]]
691 | name = "windows-sys"
692 | version = "0.36.1"
693 | source = "registry+https://github.com/rust-lang/crates.io-index"
694 | checksum = "ea04155a16a59f9eab786fe12a4a450e75cdb175f9e0d80da1e17db09f55b8d2"
695 | dependencies = [
696 | "windows_aarch64_msvc",
697 | "windows_i686_gnu",
698 | "windows_i686_msvc",
699 | "windows_x86_64_gnu",
700 | "windows_x86_64_msvc",
701 | ]
702 |
703 | [[package]]
704 | name = "windows_aarch64_msvc"
705 | version = "0.36.1"
706 | source = "registry+https://github.com/rust-lang/crates.io-index"
707 | checksum = "9bb8c3fd39ade2d67e9874ac4f3db21f0d710bee00fe7cab16949ec184eeaa47"
708 |
709 | [[package]]
710 | name = "windows_i686_gnu"
711 | version = "0.36.1"
712 | source = "registry+https://github.com/rust-lang/crates.io-index"
713 | checksum = "180e6ccf01daf4c426b846dfc66db1fc518f074baa793aa7d9b9aaeffad6a3b6"
714 |
715 | [[package]]
716 | name = "windows_i686_msvc"
717 | version = "0.36.1"
718 | source = "registry+https://github.com/rust-lang/crates.io-index"
719 | checksum = "e2e7917148b2812d1eeafaeb22a97e4813dfa60a3f8f78ebe204bcc88f12f024"
720 |
721 | [[package]]
722 | name = "windows_x86_64_gnu"
723 | version = "0.36.1"
724 | source = "registry+https://github.com/rust-lang/crates.io-index"
725 | checksum = "4dcd171b8776c41b97521e5da127a2d86ad280114807d0b2ab1e462bc764d9e1"
726 |
727 | [[package]]
728 | name = "windows_x86_64_msvc"
729 | version = "0.36.1"
730 | source = "registry+https://github.com/rust-lang/crates.io-index"
731 | checksum = "c811ca4a8c853ef420abd8592ba53ddbbac90410fab6903b3e79972a631f7680"
732 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | GNU GENERAL PUBLIC LICENSE
2 | Version 3, 29 June 2007
3 |
4 | Copyright (C) 2007 Free Software Foundation, Inc.
5 | Everyone is permitted to copy and distribute verbatim copies
6 | of this license document, but changing it is not allowed.
7 |
8 | Preamble
9 |
10 | The GNU General Public License is a free, copyleft license for
11 | software and other kinds of works.
12 |
13 | The licenses for most software and other practical works are designed
14 | to take away your freedom to share and change the works. By contrast,
15 | the GNU General Public License is intended to guarantee your freedom to
16 | share and change all versions of a program--to make sure it remains free
17 | software for all its users. We, the Free Software Foundation, use the
18 | GNU General Public License for most of our software; it applies also to
19 | any other work released this way by its authors. You can apply it to
20 | your programs, too.
21 |
22 | When we speak of free software, we are referring to freedom, not
23 | price. Our General Public Licenses are designed to make sure that you
24 | have the freedom to distribute copies of free software (and charge for
25 | them if you wish), that you receive source code or can get it if you
26 | want it, that you can change the software or use pieces of it in new
27 | free programs, and that you know you can do these things.
28 |
29 | To protect your rights, we need to prevent others from denying you
30 | these rights or asking you to surrender the rights. Therefore, you have
31 | certain responsibilities if you distribute copies of the software, or if
32 | you modify it: responsibilities to respect the freedom of others.
33 |
34 | For example, if you distribute copies of such a program, whether
35 | gratis or for a fee, you must pass on to the recipients the same
36 | freedoms that you received. You must make sure that they, too, receive
37 | or can get the source code. And you must show them these terms so they
38 | know their rights.
39 |
40 | Developers that use the GNU GPL protect your rights with two steps:
41 | (1) assert copyright on the software, and (2) offer you this License
42 | giving you legal permission to copy, distribute and/or modify it.
43 |
44 | For the developers' and authors' protection, the GPL clearly explains
45 | that there is no warranty for this free software. For both users' and
46 | authors' sake, the GPL requires that modified versions be marked as
47 | changed, so that their problems will not be attributed erroneously to
48 | authors of previous versions.
49 |
50 | Some devices are designed to deny users access to install or run
51 | modified versions of the software inside them, although the manufacturer
52 | can do so. This is fundamentally incompatible with the aim of
53 | protecting users' freedom to change the software. The systematic
54 | pattern of such abuse occurs in the area of products for individuals to
55 | use, which is precisely where it is most unacceptable. Therefore, we
56 | have designed this version of the GPL to prohibit the practice for those
57 | products. If such problems arise substantially in other domains, we
58 | stand ready to extend this provision to those domains in future versions
59 | of the GPL, as needed to protect the freedom of users.
60 |
61 | Finally, every program is threatened constantly by software patents.
62 | States should not allow patents to restrict development and use of
63 | software on general-purpose computers, but in those that do, we wish to
64 | avoid the special danger that patents applied to a free program could
65 | make it effectively proprietary. To prevent this, the GPL assures that
66 | patents cannot be used to render the program non-free.
67 |
68 | The precise terms and conditions for copying, distribution and
69 | modification follow.
70 |
71 | TERMS AND CONDITIONS
72 |
73 | 0. Definitions.
74 |
75 | "This License" refers to version 3 of the GNU General Public License.
76 |
77 | "Copyright" also means copyright-like laws that apply to other kinds of
78 | works, such as semiconductor masks.
79 |
80 | "The Program" refers to any copyrightable work licensed under this
81 | License. Each licensee is addressed as "you". "Licensees" and
82 | "recipients" may be individuals or organizations.
83 |
84 | To "modify" a work means to copy from or adapt all or part of the work
85 | in a fashion requiring copyright permission, other than the making of an
86 | exact copy. The resulting work is called a "modified version" of the
87 | earlier work or a work "based on" the earlier work.
88 |
89 | A "covered work" means either the unmodified Program or a work based
90 | on the Program.
91 |
92 | To "propagate" a work means to do anything with it that, without
93 | permission, would make you directly or secondarily liable for
94 | infringement under applicable copyright law, except executing it on a
95 | computer or modifying a private copy. Propagation includes copying,
96 | distribution (with or without modification), making available to the
97 | public, and in some countries other activities as well.
98 |
99 | To "convey" a work means any kind of propagation that enables other
100 | parties to make or receive copies. Mere interaction with a user through
101 | a computer network, with no transfer of a copy, is not conveying.
102 |
103 | An interactive user interface displays "Appropriate Legal Notices"
104 | to the extent that it includes a convenient and prominently visible
105 | feature that (1) displays an appropriate copyright notice, and (2)
106 | tells the user that there is no warranty for the work (except to the
107 | extent that warranties are provided), that licensees may convey the
108 | work under this License, and how to view a copy of this License. If
109 | the interface presents a list of user commands or options, such as a
110 | menu, a prominent item in the list meets this criterion.
111 |
112 | 1. Source Code.
113 |
114 | The "source code" for a work means the preferred form of the work
115 | for making modifications to it. "Object code" means any non-source
116 | form of a work.
117 |
118 | A "Standard Interface" means an interface that either is an official
119 | standard defined by a recognized standards body, or, in the case of
120 | interfaces specified for a particular programming language, one that
121 | is widely used among developers working in that language.
122 |
123 | The "System Libraries" of an executable work include anything, other
124 | than the work as a whole, that (a) is included in the normal form of
125 | packaging a Major Component, but which is not part of that Major
126 | Component, and (b) serves only to enable use of the work with that
127 | Major Component, or to implement a Standard Interface for which an
128 | implementation is available to the public in source code form. A
129 | "Major Component", in this context, means a major essential component
130 | (kernel, window system, and so on) of the specific operating system
131 | (if any) on which the executable work runs, or a compiler used to
132 | produce the work, or an object code interpreter used to run it.
133 |
134 | The "Corresponding Source" for a work in object code form means all
135 | the source code needed to generate, install, and (for an executable
136 | work) run the object code and to modify the work, including scripts to
137 | control those activities. However, it does not include the work's
138 | System Libraries, or general-purpose tools or generally available free
139 | programs which are used unmodified in performing those activities but
140 | which are not part of the work. For example, Corresponding Source
141 | includes interface definition files associated with source files for
142 | the work, and the source code for shared libraries and dynamically
143 | linked subprograms that the work is specifically designed to require,
144 | such as by intimate data communication or control flow between those
145 | subprograms and other parts of the work.
146 |
147 | The Corresponding Source need not include anything that users
148 | can regenerate automatically from other parts of the Corresponding
149 | Source.
150 |
151 | The Corresponding Source for a work in source code form is that
152 | same work.
153 |
154 | 2. Basic Permissions.
155 |
156 | All rights granted under this License are granted for the term of
157 | copyright on the Program, and are irrevocable provided the stated
158 | conditions are met. This License explicitly affirms your unlimited
159 | permission to run the unmodified Program. The output from running a
160 | covered work is covered by this License only if the output, given its
161 | content, constitutes a covered work. This License acknowledges your
162 | rights of fair use or other equivalent, as provided by copyright law.
163 |
164 | You may make, run and propagate covered works that you do not
165 | convey, without conditions so long as your license otherwise remains
166 | in force. You may convey covered works to others for the sole purpose
167 | of having them make modifications exclusively for you, or provide you
168 | with facilities for running those works, provided that you comply with
169 | the terms of this License in conveying all material for which you do
170 | not control copyright. Those thus making or running the covered works
171 | for you must do so exclusively on your behalf, under your direction
172 | and control, on terms that prohibit them from making any copies of
173 | your copyrighted material outside their relationship with you.
174 |
175 | Conveying under any other circumstances is permitted solely under
176 | the conditions stated below. Sublicensing is not allowed; section 10
177 | makes it unnecessary.
178 |
179 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
180 |
181 | No covered work shall be deemed part of an effective technological
182 | measure under any applicable law fulfilling obligations under article
183 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or
184 | similar laws prohibiting or restricting circumvention of such
185 | measures.
186 |
187 | When you convey a covered work, you waive any legal power to forbid
188 | circumvention of technological measures to the extent such circumvention
189 | is effected by exercising rights under this License with respect to
190 | the covered work, and you disclaim any intention to limit operation or
191 | modification of the work as a means of enforcing, against the work's
192 | users, your or third parties' legal rights to forbid circumvention of
193 | technological measures.
194 |
195 | 4. Conveying Verbatim Copies.
196 |
197 | You may convey verbatim copies of the Program's source code as you
198 | receive it, in any medium, provided that you conspicuously and
199 | appropriately publish on each copy an appropriate copyright notice;
200 | keep intact all notices stating that this License and any
201 | non-permissive terms added in accord with section 7 apply to the code;
202 | keep intact all notices of the absence of any warranty; and give all
203 | recipients a copy of this License along with the Program.
204 |
205 | You may charge any price or no price for each copy that you convey,
206 | and you may offer support or warranty protection for a fee.
207 |
208 | 5. Conveying Modified Source Versions.
209 |
210 | You may convey a work based on the Program, or the modifications to
211 | produce it from the Program, in the form of source code under the
212 | terms of section 4, provided that you also meet all of these conditions:
213 |
214 | a) The work must carry prominent notices stating that you modified
215 | it, and giving a relevant date.
216 |
217 | b) The work must carry prominent notices stating that it is
218 | released under this License and any conditions added under section
219 | 7. This requirement modifies the requirement in section 4 to
220 | "keep intact all notices".
221 |
222 | c) You must license the entire work, as a whole, under this
223 | License to anyone who comes into possession of a copy. This
224 | License will therefore apply, along with any applicable section 7
225 | additional terms, to the whole of the work, and all its parts,
226 | regardless of how they are packaged. This License gives no
227 | permission to license the work in any other way, but it does not
228 | invalidate such permission if you have separately received it.
229 |
230 | d) If the work has interactive user interfaces, each must display
231 | Appropriate Legal Notices; however, if the Program has interactive
232 | interfaces that do not display Appropriate Legal Notices, your
233 | work need not make them do so.
234 |
235 | A compilation of a covered work with other separate and independent
236 | works, which are not by their nature extensions of the covered work,
237 | and which are not combined with it such as to form a larger program,
238 | in or on a volume of a storage or distribution medium, is called an
239 | "aggregate" if the compilation and its resulting copyright are not
240 | used to limit the access or legal rights of the compilation's users
241 | beyond what the individual works permit. Inclusion of a covered work
242 | in an aggregate does not cause this License to apply to the other
243 | parts of the aggregate.
244 |
245 | 6. Conveying Non-Source Forms.
246 |
247 | You may convey a covered work in object code form under the terms
248 | of sections 4 and 5, provided that you also convey the
249 | machine-readable Corresponding Source under the terms of this License,
250 | in one of these ways:
251 |
252 | a) Convey the object code in, or embodied in, a physical product
253 | (including a physical distribution medium), accompanied by the
254 | Corresponding Source fixed on a durable physical medium
255 | customarily used for software interchange.
256 |
257 | b) Convey the object code in, or embodied in, a physical product
258 | (including a physical distribution medium), accompanied by a
259 | written offer, valid for at least three years and valid for as
260 | long as you offer spare parts or customer support for that product
261 | model, to give anyone who possesses the object code either (1) a
262 | copy of the Corresponding Source for all the software in the
263 | product that is covered by this License, on a durable physical
264 | medium customarily used for software interchange, for a price no
265 | more than your reasonable cost of physically performing this
266 | conveying of source, or (2) access to copy the
267 | Corresponding Source from a network server at no charge.
268 |
269 | c) Convey individual copies of the object code with a copy of the
270 | written offer to provide the Corresponding Source. This
271 | alternative is allowed only occasionally and noncommercially, and
272 | only if you received the object code with such an offer, in accord
273 | with subsection 6b.
274 |
275 | d) Convey the object code by offering access from a designated
276 | place (gratis or for a charge), and offer equivalent access to the
277 | Corresponding Source in the same way through the same place at no
278 | further charge. You need not require recipients to copy the
279 | Corresponding Source along with the object code. If the place to
280 | copy the object code is a network server, the Corresponding Source
281 | may be on a different server (operated by you or a third party)
282 | that supports equivalent copying facilities, provided you maintain
283 | clear directions next to the object code saying where to find the
284 | Corresponding Source. Regardless of what server hosts the
285 | Corresponding Source, you remain obligated to ensure that it is
286 | available for as long as needed to satisfy these requirements.
287 |
288 | e) Convey the object code using peer-to-peer transmission, provided
289 | you inform other peers where the object code and Corresponding
290 | Source of the work are being offered to the general public at no
291 | charge under subsection 6d.
292 |
293 | A separable portion of the object code, whose source code is excluded
294 | from the Corresponding Source as a System Library, need not be
295 | included in conveying the object code work.
296 |
297 | A "User Product" is either (1) a "consumer product", which means any
298 | tangible personal property which is normally used for personal, family,
299 | or household purposes, or (2) anything designed or sold for incorporation
300 | into a dwelling. In determining whether a product is a consumer product,
301 | doubtful cases shall be resolved in favor of coverage. For a particular
302 | product received by a particular user, "normally used" refers to a
303 | typical or common use of that class of product, regardless of the status
304 | of the particular user or of the way in which the particular user
305 | actually uses, or expects or is expected to use, the product. A product
306 | is a consumer product regardless of whether the product has substantial
307 | commercial, industrial or non-consumer uses, unless such uses represent
308 | the only significant mode of use of the product.
309 |
310 | "Installation Information" for a User Product means any methods,
311 | procedures, authorization keys, or other information required to install
312 | and execute modified versions of a covered work in that User Product from
313 | a modified version of its Corresponding Source. The information must
314 | suffice to ensure that the continued functioning of the modified object
315 | code is in no case prevented or interfered with solely because
316 | modification has been made.
317 |
318 | If you convey an object code work under this section in, or with, or
319 | specifically for use in, a User Product, and the conveying occurs as
320 | part of a transaction in which the right of possession and use of the
321 | User Product is transferred to the recipient in perpetuity or for a
322 | fixed term (regardless of how the transaction is characterized), the
323 | Corresponding Source conveyed under this section must be accompanied
324 | by the Installation Information. But this requirement does not apply
325 | if neither you nor any third party retains the ability to install
326 | modified object code on the User Product (for example, the work has
327 | been installed in ROM).
328 |
329 | The requirement to provide Installation Information does not include a
330 | requirement to continue to provide support service, warranty, or updates
331 | for a work that has been modified or installed by the recipient, or for
332 | the User Product in which it has been modified or installed. Access to a
333 | network may be denied when the modification itself materially and
334 | adversely affects the operation of the network or violates the rules and
335 | protocols for communication across the network.
336 |
337 | Corresponding Source conveyed, and Installation Information provided,
338 | in accord with this section must be in a format that is publicly
339 | documented (and with an implementation available to the public in
340 | source code form), and must require no special password or key for
341 | unpacking, reading or copying.
342 |
343 | 7. Additional Terms.
344 |
345 | "Additional permissions" are terms that supplement the terms of this
346 | License by making exceptions from one or more of its conditions.
347 | Additional permissions that are applicable to the entire Program shall
348 | be treated as though they were included in this License, to the extent
349 | that they are valid under applicable law. If additional permissions
350 | apply only to part of the Program, that part may be used separately
351 | under those permissions, but the entire Program remains governed by
352 | this License without regard to the additional permissions.
353 |
354 | When you convey a copy of a covered work, you may at your option
355 | remove any additional permissions from that copy, or from any part of
356 | it. (Additional permissions may be written to require their own
357 | removal in certain cases when you modify the work.) You may place
358 | additional permissions on material, added by you to a covered work,
359 | for which you have or can give appropriate copyright permission.
360 |
361 | Notwithstanding any other provision of this License, for material you
362 | add to a covered work, you may (if authorized by the copyright holders of
363 | that material) supplement the terms of this License with terms:
364 |
365 | a) Disclaiming warranty or limiting liability differently from the
366 | terms of sections 15 and 16 of this License; or
367 |
368 | b) Requiring preservation of specified reasonable legal notices or
369 | author attributions in that material or in the Appropriate Legal
370 | Notices displayed by works containing it; or
371 |
372 | c) Prohibiting misrepresentation of the origin of that material, or
373 | requiring that modified versions of such material be marked in
374 | reasonable ways as different from the original version; or
375 |
376 | d) Limiting the use for publicity purposes of names of licensors or
377 | authors of the material; or
378 |
379 | e) Declining to grant rights under trademark law for use of some
380 | trade names, trademarks, or service marks; or
381 |
382 | f) Requiring indemnification of licensors and authors of that
383 | material by anyone who conveys the material (or modified versions of
384 | it) with contractual assumptions of liability to the recipient, for
385 | any liability that these contractual assumptions directly impose on
386 | those licensors and authors.
387 |
388 | All other non-permissive additional terms are considered "further
389 | restrictions" within the meaning of section 10. If the Program as you
390 | received it, or any part of it, contains a notice stating that it is
391 | governed by this License along with a term that is a further
392 | restriction, you may remove that term. If a license document contains
393 | a further restriction but permits relicensing or conveying under this
394 | License, you may add to a covered work material governed by the terms
395 | of that license document, provided that the further restriction does
396 | not survive such relicensing or conveying.
397 |
398 | If you add terms to a covered work in accord with this section, you
399 | must place, in the relevant source files, a statement of the
400 | additional terms that apply to those files, or a notice indicating
401 | where to find the applicable terms.
402 |
403 | Additional terms, permissive or non-permissive, may be stated in the
404 | form of a separately written license, or stated as exceptions;
405 | the above requirements apply either way.
406 |
407 | 8. Termination.
408 |
409 | You may not propagate or modify a covered work except as expressly
410 | provided under this License. Any attempt otherwise to propagate or
411 | modify it is void, and will automatically terminate your rights under
412 | this License (including any patent licenses granted under the third
413 | paragraph of section 11).
414 |
415 | However, if you cease all violation of this License, then your
416 | license from a particular copyright holder is reinstated (a)
417 | provisionally, unless and until the copyright holder explicitly and
418 | finally terminates your license, and (b) permanently, if the copyright
419 | holder fails to notify you of the violation by some reasonable means
420 | prior to 60 days after the cessation.
421 |
422 | Moreover, your license from a particular copyright holder is
423 | reinstated permanently if the copyright holder notifies you of the
424 | violation by some reasonable means, this is the first time you have
425 | received notice of violation of this License (for any work) from that
426 | copyright holder, and you cure the violation prior to 30 days after
427 | your receipt of the notice.
428 |
429 | Termination of your rights under this section does not terminate the
430 | licenses of parties who have received copies or rights from you under
431 | this License. If your rights have been terminated and not permanently
432 | reinstated, you do not qualify to receive new licenses for the same
433 | material under section 10.
434 |
435 | 9. Acceptance Not Required for Having Copies.
436 |
437 | You are not required to accept this License in order to receive or
438 | run a copy of the Program. Ancillary propagation of a covered work
439 | occurring solely as a consequence of using peer-to-peer transmission
440 | to receive a copy likewise does not require acceptance. However,
441 | nothing other than this License grants you permission to propagate or
442 | modify any covered work. These actions infringe copyright if you do
443 | not accept this License. Therefore, by modifying or propagating a
444 | covered work, you indicate your acceptance of this License to do so.
445 |
446 | 10. Automatic Licensing of Downstream Recipients.
447 |
448 | Each time you convey a covered work, the recipient automatically
449 | receives a license from the original licensors, to run, modify and
450 | propagate that work, subject to this License. You are not responsible
451 | for enforcing compliance by third parties with this License.
452 |
453 | An "entity transaction" is a transaction transferring control of an
454 | organization, or substantially all assets of one, or subdividing an
455 | organization, or merging organizations. If propagation of a covered
456 | work results from an entity transaction, each party to that
457 | transaction who receives a copy of the work also receives whatever
458 | licenses to the work the party's predecessor in interest had or could
459 | give under the previous paragraph, plus a right to possession of the
460 | Corresponding Source of the work from the predecessor in interest, if
461 | the predecessor has it or can get it with reasonable efforts.
462 |
463 | You may not impose any further restrictions on the exercise of the
464 | rights granted or affirmed under this License. For example, you may
465 | not impose a license fee, royalty, or other charge for exercise of
466 | rights granted under this License, and you may not initiate litigation
467 | (including a cross-claim or counterclaim in a lawsuit) alleging that
468 | any patent claim is infringed by making, using, selling, offering for
469 | sale, or importing the Program or any portion of it.
470 |
471 | 11. Patents.
472 |
473 | A "contributor" is a copyright holder who authorizes use under this
474 | License of the Program or a work on which the Program is based. The
475 | work thus licensed is called the contributor's "contributor version".
476 |
477 | A contributor's "essential patent claims" are all patent claims
478 | owned or controlled by the contributor, whether already acquired or
479 | hereafter acquired, that would be infringed by some manner, permitted
480 | by this License, of making, using, or selling its contributor version,
481 | but do not include claims that would be infringed only as a
482 | consequence of further modification of the contributor version. For
483 | purposes of this definition, "control" includes the right to grant
484 | patent sublicenses in a manner consistent with the requirements of
485 | this License.
486 |
487 | Each contributor grants you a non-exclusive, worldwide, royalty-free
488 | patent license under the contributor's essential patent claims, to
489 | make, use, sell, offer for sale, import and otherwise run, modify and
490 | propagate the contents of its contributor version.
491 |
492 | In the following three paragraphs, a "patent license" is any express
493 | agreement or commitment, however denominated, not to enforce a patent
494 | (such as an express permission to practice a patent or covenant not to
495 | sue for patent infringement). To "grant" such a patent license to a
496 | party means to make such an agreement or commitment not to enforce a
497 | patent against the party.
498 |
499 | If you convey a covered work, knowingly relying on a patent license,
500 | and the Corresponding Source of the work is not available for anyone
501 | to copy, free of charge and under the terms of this License, through a
502 | publicly available network server or other readily accessible means,
503 | then you must either (1) cause the Corresponding Source to be so
504 | available, or (2) arrange to deprive yourself of the benefit of the
505 | patent license for this particular work, or (3) arrange, in a manner
506 | consistent with the requirements of this License, to extend the patent
507 | license to downstream recipients. "Knowingly relying" means you have
508 | actual knowledge that, but for the patent license, your conveying the
509 | covered work in a country, or your recipient's use of the covered work
510 | in a country, would infringe one or more identifiable patents in that
511 | country that you have reason to believe are valid.
512 |
513 | If, pursuant to or in connection with a single transaction or
514 | arrangement, you convey, or propagate by procuring conveyance of, a
515 | covered work, and grant a patent license to some of the parties
516 | receiving the covered work authorizing them to use, propagate, modify
517 | or convey a specific copy of the covered work, then the patent license
518 | you grant is automatically extended to all recipients of the covered
519 | work and works based on it.
520 |
521 | A patent license is "discriminatory" if it does not include within
522 | the scope of its coverage, prohibits the exercise of, or is
523 | conditioned on the non-exercise of one or more of the rights that are
524 | specifically granted under this License. You may not convey a covered
525 | work if you are a party to an arrangement with a third party that is
526 | in the business of distributing software, under which you make payment
527 | to the third party based on the extent of your activity of conveying
528 | the work, and under which the third party grants, to any of the
529 | parties who would receive the covered work from you, a discriminatory
530 | patent license (a) in connection with copies of the covered work
531 | conveyed by you (or copies made from those copies), or (b) primarily
532 | for and in connection with specific products or compilations that
533 | contain the covered work, unless you entered into that arrangement,
534 | or that patent license was granted, prior to 28 March 2007.
535 |
536 | Nothing in this License shall be construed as excluding or limiting
537 | any implied license or other defenses to infringement that may
538 | otherwise be available to you under applicable patent law.
539 |
540 | 12. No Surrender of Others' Freedom.
541 |
542 | If conditions are imposed on you (whether by court order, agreement or
543 | otherwise) that contradict the conditions of this License, they do not
544 | excuse you from the conditions of this License. If you cannot convey a
545 | covered work so as to satisfy simultaneously your obligations under this
546 | License and any other pertinent obligations, then as a consequence you may
547 | not convey it at all. For example, if you agree to terms that obligate you
548 | to collect a royalty for further conveying from those to whom you convey
549 | the Program, the only way you could satisfy both those terms and this
550 | License would be to refrain entirely from conveying the Program.
551 |
552 | 13. Use with the GNU Affero General Public License.
553 |
554 | Notwithstanding any other provision of this License, you have
555 | permission to link or combine any covered work with a work licensed
556 | under version 3 of the GNU Affero General Public License into a single
557 | combined work, and to convey the resulting work. The terms of this
558 | License will continue to apply to the part which is the covered work,
559 | but the special requirements of the GNU Affero General Public License,
560 | section 13, concerning interaction through a network will apply to the
561 | combination as such.
562 |
563 | 14. Revised Versions of this License.
564 |
565 | The Free Software Foundation may publish revised and/or new versions of
566 | the GNU General Public License from time to time. Such new versions will
567 | be similar in spirit to the present version, but may differ in detail to
568 | address new problems or concerns.
569 |
570 | Each version is given a distinguishing version number. If the
571 | Program specifies that a certain numbered version of the GNU General
572 | Public License "or any later version" applies to it, you have the
573 | option of following the terms and conditions either of that numbered
574 | version or of any later version published by the Free Software
575 | Foundation. If the Program does not specify a version number of the
576 | GNU General Public License, you may choose any version ever published
577 | by the Free Software Foundation.
578 |
579 | If the Program specifies that a proxy can decide which future
580 | versions of the GNU General Public License can be used, that proxy's
581 | public statement of acceptance of a version permanently authorizes you
582 | to choose that version for the Program.
583 |
584 | Later license versions may give you additional or different
585 | permissions. However, no additional obligations are imposed on any
586 | author or copyright holder as a result of your choosing to follow a
587 | later version.
588 |
589 | 15. Disclaimer of Warranty.
590 |
591 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
592 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
593 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
594 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
595 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
596 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
597 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
598 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
599 |
600 | 16. Limitation of Liability.
601 |
602 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
603 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
604 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
605 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
606 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
607 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
608 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
609 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
610 | SUCH DAMAGES.
611 |
612 | 17. Interpretation of Sections 15 and 16.
613 |
614 | If the disclaimer of warranty and limitation of liability provided
615 | above cannot be given local legal effect according to their terms,
616 | reviewing courts shall apply local law that most closely approximates
617 | an absolute waiver of all civil liability in connection with the
618 | Program, unless a warranty or assumption of liability accompanies a
619 | copy of the Program in return for a fee.
620 |
621 | END OF TERMS AND CONDITIONS
622 |
623 | How to Apply These Terms to Your New Programs
624 |
625 | If you develop a new program, and you want it to be of the greatest
626 | possible use to the public, the best way to achieve this is to make it
627 | free software which everyone can redistribute and change under these terms.
628 |
629 | To do so, attach the following notices to the program. It is safest
630 | to attach them to the start of each source file to most effectively
631 | state the exclusion of warranty; and each file should have at least
632 | the "copyright" line and a pointer to where the full notice is found.
633 |
634 |
635 | Copyright (C)
636 |
637 | This program is free software: you can redistribute it and/or modify
638 | it under the terms of the GNU General Public License as published by
639 | the Free Software Foundation, either version 3 of the License, or
640 | (at your option) any later version.
641 |
642 | This program is distributed in the hope that it will be useful,
643 | but WITHOUT ANY WARRANTY; without even the implied warranty of
644 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
645 | GNU General Public License for more details.
646 |
647 | You should have received a copy of the GNU General Public License
648 | along with this program. If not, see .
649 |
650 | Also add information on how to contact you by electronic and paper mail.
651 |
652 | If the program does terminal interaction, make it output a short
653 | notice like this when it starts in an interactive mode:
654 |
655 | Copyright (C)
656 | This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
657 | This is free software, and you are welcome to redistribute it
658 | under certain conditions; type `show c' for details.
659 |
660 | The hypothetical commands `show w' and `show c' should show the appropriate
661 | parts of the General Public License. Of course, your program's commands
662 | might be different; for a GUI interface, you would use an "about box".
663 |
664 | You should also get your employer (if you work as a programmer) or school,
665 | if any, to sign a "copyright disclaimer" for the program, if necessary.
666 | For more information on this, and how to apply and follow the GNU GPL, see
667 | .
668 |
669 | The GNU General Public License does not permit incorporating your program
670 | into proprietary programs. If your program is a subroutine library, you
671 | may consider it more useful to permit linking proprietary applications with
672 | the library. If this is what you want to do, use the GNU Lesser General
673 | Public License instead of this License. But first, please read
674 | .
675 |
--------------------------------------------------------------------------------