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
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
use crate::build::BuildDirectory;
use crate::cmd::{Command, SandboxImage};
use crate::inside_docker::CurrentContainer;
use crate::Toolchain;
use failure::{Error, ResultExt};
use log::info;
use remove_dir_all::remove_dir_all;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use std::time::Duration;

#[cfg(windows)]
static DEFAULT_SANDBOX_IMAGE: &str = "rustops/crates-build-env-windows";

#[cfg(not(windows))]
static DEFAULT_SANDBOX_IMAGE: &str = "rustops/crates-build-env";

const DEFAULT_COMMAND_TIMEOUT: Option<Duration> = Some(Duration::from_secs(15 * 60));
const DEFAULT_COMMAND_NO_OUTPUT_TIMEOUT: Option<Duration> = None;

static DEFAULT_RUSTUP_PROFILE: &str = "minimal";

/// Builder of a [`Workspace`](struct.Workspace.html).
pub struct WorkspaceBuilder {
    user_agent: String,
    path: PathBuf,
    sandbox_image: Option<SandboxImage>,
    command_timeout: Option<Duration>,
    command_no_output_timeout: Option<Duration>,
    fetch_registry_index_during_builds: bool,
    running_inside_docker: bool,
    fast_init: bool,
    rustup_profile: String,
}

impl WorkspaceBuilder {
    /// Create a new builder.
    ///
    /// The provided path will be the home of the workspace, containing all the data generated by
    /// rustwide (including state and caches).
    pub fn new(path: &Path, user_agent: &str) -> Self {
        Self {
            user_agent: user_agent.into(),
            path: path.into(),
            sandbox_image: None,
            command_timeout: DEFAULT_COMMAND_TIMEOUT,
            command_no_output_timeout: DEFAULT_COMMAND_NO_OUTPUT_TIMEOUT,
            fetch_registry_index_during_builds: true,
            running_inside_docker: false,
            fast_init: false,
            rustup_profile: DEFAULT_RUSTUP_PROFILE.into(),
        }
    }

    /// Override the image used for sandboxes.
    ///
    /// By default rustwide will use the [rustops/crates-build-env] image on Linux systems, and
    /// [rustops/crates-build-env-windows] on Windows systems. Those images contain dependencies to
    /// build a large amount of crates.
    ///
    /// [rustops/crates-build-env]: https://hub.docker.com/r/rustops/crates-build-env
    /// [rustops/crates-build-env-windows]: https://hub.docker.com/r/rustops/crates-build-env-windows
    pub fn sandbox_image(mut self, image: SandboxImage) -> Self {
        self.sandbox_image = Some(image);
        self
    }

    /// Set the default timeout of [`Command`](cmd/struct.Command.html), which can be overridden
    /// with the [`Command::timeout`](cmd/struct.Command.html#method.timeout) method. To disable
    /// the timeout set its value to `None`. By default the timeout is 15 minutes.
    pub fn command_timeout(mut self, timeout: Option<Duration>) -> Self {
        self.command_timeout = timeout;
        self
    }

    /// Set the default no output timeout of [`Command`](cmd/struct.Command.html), which can be
    /// overridden with the
    /// [`Command::no_output_timeout`](cmd/struct.Command.html#method.no_output_timeout) method. To
    /// disable the timeout set its value to `None`. By default it's disabled.
    pub fn command_no_output_timeout(mut self, timeout: Option<Duration>) -> Self {
        self.command_no_output_timeout = timeout;
        self
    }

    /// Enable or disable fast workspace initialization (disabled by default).
    ///
    /// Fast workspace initialization will change the initialization process to prefer
    /// initialization speed to runtime performance, for example by installing the tools rustwide
    /// needs in debug mode instead of release mode. It's not recommended to enable fast workspace
    /// initialization with production workloads, but it can help in CIs or other automated testing
    /// scenarios.
    pub fn fast_init(mut self, enable: bool) -> Self {
        self.fast_init = enable;
        self
    }

    /// Enable or disable fetching the registry's index during each build (enabled by default).
    ///
    /// When this option is disabled the index will only be fetched when the workspace is
    /// initialized, and no following build do that again. It's useful to disable it when you need
    /// to build a lot of crates in a batch, but having the option disabled might cause trouble if
    /// you need to build recently published crates, as they might be missing from the cached
    /// index.
    ///
    /// **To call this method the `unstable` rustwide feature flag needs to be enabled**, as it
    /// relies on unstable Cargo features.
    #[cfg(feature = "unstable")]
    pub fn fetch_registry_index_during_builds(mut self, enable: bool) -> Self {
        self.fetch_registry_index_during_builds = enable;
        self
    }

    /// Enable or disable support for running Rustwide itself inside Docker (disabled by default).
    ///
    /// When support is enabled Rustwide will try to detect whether it's actually running inside a
    /// Docker container during initialization, and in that case it will adapt itself. This is
    /// needed because starting a sibling container from another one requires mount sources to be
    /// remapped to the real directory on the host.
    ///
    /// Other than enabling support for it, to run Rustwide inside Docker your container needs to
    /// meet these requirements:
    ///
    /// * The Docker socker (`/var/run/docker.sock`) needs to be mounted inside the container.
    /// * The workspace directory must be either mounted from the host system or in a child
    ///   directory of a mount from the host system. Workspaces created inside the container are
    ///   not supported.
    pub fn running_inside_docker(mut self, inside: bool) -> Self {
        self.running_inside_docker = inside;
        self
    }

    /// Name of the rustup profile used when installing toolchains. The default is `minimal`.
    pub fn rustup_profile(mut self, profile: &str) -> Self {
        self.rustup_profile = profile.into();
        self
    }

    /// Initialize the workspace. This will create all the necessary local files and fetch the rest from the network. It's
    /// not unexpected for this method to take minutes to run on slower network connections.
    pub fn init(self) -> Result<Workspace, Error> {
        std::fs::create_dir_all(&self.path).with_context(|_| {
            format!(
                "failed to create workspace directory: {}",
                self.path.display()
            )
        })?;

        crate::utils::file_lock(&self.path.join("lock"), "initialize the workspace", || {
            let sandbox_image = if let Some(img) = self.sandbox_image {
                img
            } else {
                SandboxImage::remote(DEFAULT_SANDBOX_IMAGE)?
            };

            let mut headers = reqwest::header::HeaderMap::new();
            headers.insert(reqwest::header::USER_AGENT, self.user_agent.parse()?);
            let http = reqwest::ClientBuilder::new()
                .default_headers(headers)
                .build()?;

            let mut ws = Workspace {
                inner: Arc::new(WorkspaceInner {
                    http,
                    path: self.path,
                    sandbox_image,
                    command_timeout: self.command_timeout,
                    command_no_output_timeout: self.command_no_output_timeout,
                    fetch_registry_index_during_builds: self.fetch_registry_index_during_builds,
                    current_container: None,
                    rustup_profile: self.rustup_profile,
                }),
            };

            if self.running_inside_docker {
                let container = CurrentContainer::detect(&ws)?;
                Arc::get_mut(&mut ws.inner).unwrap().current_container = container;
            }

            ws.init(self.fast_init)?;
            Ok(ws)
        })
    }
}

struct WorkspaceInner {
    http: reqwest::Client,
    path: PathBuf,
    sandbox_image: SandboxImage,
    command_timeout: Option<Duration>,
    command_no_output_timeout: Option<Duration>,
    fetch_registry_index_during_builds: bool,
    current_container: Option<CurrentContainer>,
    rustup_profile: String,
}

/// Directory on the filesystem containing rustwide's state and caches.
///
/// Use [`WorkspaceBuilder`](struct.WorkspaceBuilder.html) to create a new instance of it.
pub struct Workspace {
    inner: Arc<WorkspaceInner>,
}

impl Workspace {
    /// Open a named build directory inside the workspace.
    pub fn build_dir(&self, name: &str) -> BuildDirectory {
        BuildDirectory::new(
            Workspace {
                inner: self.inner.clone(),
            },
            name,
        )
    }

    /// Remove all the contents of all the build directories, freeing disk space.
    pub fn purge_all_build_dirs(&self) -> Result<(), Error> {
        let dir = self.builds_dir();
        if dir.exists() {
            remove_dir_all(&dir)?;
        }
        Ok(())
    }

    /// Return a list of all the toolchains present in the workspace.
    ///
    /// # Example
    ///
    /// This code snippet removes all the installed toolchains except the main one:
    ///
    /// ```no_run
    /// # use rustwide::{WorkspaceBuilder, Toolchain};
    /// # use std::error::Error;
    /// # fn main() -> Result<(), Box<dyn Error>> {
    /// # let workspace = WorkspaceBuilder::new("".as_ref(), "").init()?;
    /// let main_toolchain = Toolchain::dist("stable");
    /// for installed in &workspace.installed_toolchains()? {
    ///     if *installed != main_toolchain {
    ///         installed.uninstall(&workspace)?;
    ///     }
    /// }
    /// # Ok(())
    /// # }
    /// ```
    pub fn installed_toolchains(&self) -> Result<Vec<Toolchain>, Error> {
        crate::toolchain::list_installed_toolchains(&self.rustup_home())
    }

    pub(crate) fn http_client(&self) -> &reqwest::Client {
        &self.inner.http
    }

    pub(crate) fn cargo_home(&self) -> PathBuf {
        self.inner.path.join("cargo-home")
    }

    pub(crate) fn rustup_home(&self) -> PathBuf {
        self.inner.path.join("rustup-home")
    }

    pub(crate) fn cache_dir(&self) -> PathBuf {
        self.inner.path.join("cache")
    }

    pub(crate) fn builds_dir(&self) -> PathBuf {
        self.inner.path.join("builds")
    }

    pub(crate) fn sandbox_image(&self) -> &SandboxImage {
        &self.inner.sandbox_image
    }

    pub(crate) fn default_command_timeout(&self) -> Option<Duration> {
        self.inner.command_timeout
    }

    pub(crate) fn default_command_no_output_timeout(&self) -> Option<Duration> {
        self.inner.command_no_output_timeout
    }

    pub(crate) fn fetch_registry_index_during_builds(&self) -> bool {
        self.inner.fetch_registry_index_during_builds
    }

    pub(crate) fn current_container(&self) -> Option<&CurrentContainer> {
        self.inner.current_container.as_ref()
    }

    pub(crate) fn rustup_profile(&self) -> &str {
        &self.inner.rustup_profile
    }

    fn init(&self, fast_init: bool) -> Result<(), Error> {
        info!("installing tools required by rustwide");
        crate::tools::install(self, fast_init)?;
        if !self.fetch_registry_index_during_builds() {
            info!("updating the local crates.io registry clone");
            self.update_cratesio_registry()?;
        }
        Ok(())
    }

    fn update_cratesio_registry(&self) -> Result<(), Error> {
        // This nop cargo command is to update the registry so we don't have to do it for each
        // crate.  using `install` is a temporary solution until
        // https://github.com/rust-lang/cargo/pull/5961 is ready

        let _ = Command::new(self, Toolchain::MAIN.cargo())
            .args(&["install", "lazy_static"])
            .no_output_timeout(None)
            .run();

        // ignore the error untill https://github.com/rust-lang/cargo/pull/5961 is ready
        Ok(())
    }
}