1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
use std::{
    collections::{HashMap, HashSet},
    fs,
    io::Write,
    path::Path, time::Duration, thread,
};

use cargo::{
    core::{resolver::CliFeatures, Workspace},
    ops::{Packages, PublishOpts},
    Config,
};
use retry::{delay, retry_with_index};
use semver::Version;
use serde::{Deserialize, Serialize};
use toml_edit::Document;

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ArmoryTOML {
    pub version: Version,
}

pub fn load_armory_toml(workspace_dir: &Path) -> Result<ArmoryTOML, String> {
    toml::from_slice(
        &fs::read_to_string(workspace_dir.join("armory.toml"))
            .unwrap()
            .as_bytes(),
    )
    .map_err(|_| "Failed to parse armory.toml".to_string())
}

pub fn save_armory_toml(workspace_dir: &Path, armory_toml: &ArmoryTOML) {
    let mut file = fs::File::create(workspace_dir.join("armory.toml")).unwrap();
    file.write_all(toml::to_string(armory_toml).unwrap().as_bytes())
        .unwrap();
}

#[derive(Debug, Clone, Serialize, Deserialize)]
struct WorkspaceManifest {
    pub workspace: WorkspaceDefinition,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
struct WorkspaceDefinition {
    pub members: Vec<String>,
}

fn update_member_deps(dir: &Path, version: &Version) -> HashMap<String, HashSet<String>>{
    // directed acyclic graph to figure out which dependencies
    // to publish first.
    let mut graph: HashMap<String, HashSet<String>> = HashMap::new();

    let workspace_toml: WorkspaceManifest = toml::from_slice(
        &fs::read_to_string(dir.join("Cargo.toml"))
            .unwrap()
            .as_bytes(),
    ).unwrap();

    for member in workspace_toml.workspace.members {
        let member_dir = dir.join(&member);
        let member_toml = fs::read_to_string(member_dir.join("Cargo.toml")).unwrap();
        let mut member_toml = member_toml.parse::<Document>().unwrap();
        let mut local_deps = HashSet::new();

        member_toml["package"]["version"] = toml_edit::value(version.to_string());
        let deps = member_toml.get_mut("dependencies").map(|deps| deps.as_table_mut());
        match deps {
            Some(Some(table)) => {
                for (name, dep) in table.iter_mut() {
                    if let Some(dep) = dep.as_table_like_mut() {
                        if let Some(Some(_)) = dep.get("path").map(|dep| dep.as_str()) {
                            // this is a local dependency, so we will need to update the version
                            dep.insert("version", toml_edit::value(version.to_string()));
                            local_deps.insert(name.trim().into());
                        }
                    }
                }
            }
            _ => {}
        }

        let mut file = fs::File::create(member_dir.join("Cargo.toml")).unwrap();
        file.write_all(member_toml.to_string().as_bytes()).unwrap();


        graph.insert(member.trim().into(), local_deps);
    }

    // now we have a graph of dependencies, we can figure out which
    // dependencies to publish first, in the next stage
    graph
}

pub fn publish_workspace(dir: &Path, version: &Version) {

    let graph = update_member_deps(dir, version);

    let mut cfg = Config::default().unwrap();
    cfg.set_values(cfg.load_values().unwrap()).unwrap();
    cfg.load_credentials().unwrap();
    let workspace = Workspace::new(&dir.clone().join("Cargo.toml"), &cfg).unwrap();

    let mut already_published: HashSet<String> = HashSet::new();
    dbg!(&graph);
    for current_package in graph.keys() {
        publish_crate(
            &workspace,
            &cfg,
            current_package,
            &graph,
            &mut already_published,
        )
    }
}

fn publish_crate(
    workspace: &Workspace,
    cfg: &Config,
    current_package: &str,
    all_packages: &HashMap<String, HashSet<String>>,
    already_published: &mut HashSet<String>,
) {
    if already_published.contains(current_package) {
        return;
    }
    // publish all the local dependencies first
    for local_dep in all_packages.get(current_package).unwrap() {
        if !already_published.contains(local_dep) {
            publish_crate(workspace, cfg, local_dep, all_packages, already_published);
        }
    }

    retry_with_index(delay::Fibonacci::from_millis(3000), |current_try| {
        match cargo::ops::publish(
            workspace,
            &PublishOpts {
                token: None,
                config: &cfg,
                verify: false,
                allow_dirty: true,
                registry: None,
                dry_run: false,
                targets: vec![],
                to_publish: Packages::Packages(vec![current_package.to_string()]),
                cli_features: CliFeatures::new_all(true),
                index: None,
                jobs: None,
            },
        ) {
            Ok(_) => Ok(()),
            Err(e) => {
                if current_try > 5{
                    panic!("ARMORY: failed to publish {} after {} attempts: {:#?}",
                            current_package, current_try, e);
                } else {
                    println!("ARMORY: failed to publish {} after {} attempts: {:#?}",
                        current_package, current_try, e);
                }
                Err(e)
            }
        }
    })
    .unwrap();

    // sleep for a bit to let the crates.io index catch up
    thread::sleep(Duration::from_millis(3000));

    already_published.insert(current_package.to_string());
}