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
//! Provides functions to build the kernel and the bootloader.

use crate::config::Config;
use cargo_metadata::Metadata;
use error::{BootloaderError, BuildKernelError, BuilderError, CreateBootimageError};
use std::{
    path::{Path, PathBuf},
    process,
};

/// Provides the build command for the bootloader.
mod bootloader;
/// Provides a function to create the bootable disk image.
mod disk_image;
/// Contains the errors types returned by the `Builder` methods.
pub mod error;

/// Allows building the kernel and creating a bootable disk image with it.
pub struct Builder {
    manifest_path: PathBuf,
    project_metadata: Option<Metadata>,
}

impl Builder {
    /// Creates a new builder for the project at the given manifest path
    ///
    /// If None is passed for `manifest_path`, it is automatically searched.
    pub fn new(manifest_path: Option<PathBuf>) -> Result<Self, BuilderError> {
        let manifest_path = match manifest_path.or_else(|| {
            std::env::var("CARGO_MANIFEST_DIR")
                .ok()
                .map(|dir| Path::new(&dir).join("Cargo.toml"))
        }) {
            Some(path) => path,
            None => {
                println!("WARNING: `CARGO_MANIFEST_DIR` env variable not set");
                locate_cargo_manifest::locate_manifest()?
            }
        };

        Ok(Builder {
            manifest_path,
            project_metadata: None,
        })
    }

    /// Returns the path to the Cargo.toml file of the project.
    pub fn manifest_path(&self) -> &Path {
        &self.manifest_path
    }

    /// Builds the kernel by executing `cargo build` with the given arguments.
    ///
    /// Returns a list of paths to all built executables. For crates with only a single binary,
    /// the returned list contains only a single element.
    ///
    /// If the quiet argument is set to true, all output to stdout is suppressed.
    pub fn build_kernel(
        &mut self,
        args: &[String],
        config: &Config,
        quiet: bool,
    ) -> Result<Vec<PathBuf>, BuildKernelError> {
        if !quiet {
            println!("Building kernel");
        }

        // try to build kernel
        let cargo = std::env::var("CARGO").unwrap_or_else(|_| "cargo".to_owned());
        let mut cmd = process::Command::new(&cargo);
        cmd.args(&config.build_command);
        cmd.args(args);
        if !quiet {
            cmd.stdout(process::Stdio::inherit());
            cmd.stderr(process::Stdio::inherit());
        }
        let output = cmd.output().map_err(|err| BuildKernelError::Io {
            message: "failed to execute kernel build",
            error: err,
        })?;
        if !output.status.success() {
            if config.build_command.starts_with(&["xbuild".into()]) {
                // try executing `cargo xbuild --help` to check whether cargo-xbuild is installed
                let mut help_command = process::Command::new("cargo");
                help_command.arg("xbuild").arg("--help");
                help_command.stdout(process::Stdio::null());
                help_command.stderr(process::Stdio::null());
                if let Ok(help_exit_status) = help_command.status() {
                    if !help_exit_status.success() {
                        return Err(BuildKernelError::XbuildNotFound);
                    }
                }
            }
            return Err(BuildKernelError::BuildFailed {
                stderr: output.stderr,
            });
        }

        // Retrieve binary paths
        let mut cmd = process::Command::new(cargo);
        cmd.args(&config.build_command);
        cmd.args(args);
        cmd.arg("--message-format").arg("json");
        let output = cmd.output().map_err(|err| BuildKernelError::Io {
            message: "failed to execute kernel build with json output",
            error: err,
        })?;
        if !output.status.success() {
            return Err(BuildKernelError::BuildFailed {
                stderr: output.stderr,
            });
        }
        let mut executables = Vec::new();
        for line in String::from_utf8(output.stdout)
            .map_err(BuildKernelError::BuildJsonOutputInvalidUtf8)?
            .lines()
        {
            let mut artifact =
                json::parse(line).map_err(BuildKernelError::BuildJsonOutputInvalidJson)?;
            if let Some(executable) = artifact["executable"].take_string() {
                executables.push(PathBuf::from(executable));
            }
        }

        Ok(executables)
    }

    /// Creates a bootimage by combining the given kernel binary with the bootloader.
    ///
    /// Places the resulting bootable disk image at the given `output_bin_path`.
    ///
    /// If the quiet argument is set to true, all output to stdout is suppressed.
    pub fn create_bootimage(
        &mut self,
        kernel_manifest_path: &Path,
        bin_path: &Path,
        output_bin_path: &Path,
        quiet: bool,
    ) -> Result<(), CreateBootimageError> {
        let bootloader_build_config = bootloader::BuildConfig::from_metadata(
            self.project_metadata()?,
            kernel_manifest_path,
            bin_path,
        )?;

        // build bootloader
        if !quiet {
            println!("Building bootloader");
        }
        let mut cmd = bootloader_build_config.build_command();
        if !quiet {
            cmd.stdout(process::Stdio::inherit());
            cmd.stderr(process::Stdio::inherit());
        }
        let output = cmd.output().map_err(|err| CreateBootimageError::Io {
            message: "failed to execute bootloader build command",
            error: err,
        })?;
        if !output.status.success() {
            return Err(CreateBootimageError::BootloaderBuildFailed {
                stderr: output.stderr,
            });
        }

        // Retrieve binary path
        let mut cmd = bootloader_build_config.build_command();
        cmd.arg("--message-format").arg("json");
        let output = cmd.output().map_err(|err| CreateBootimageError::Io {
            message: "failed to execute bootloader build command with json output",
            error: err,
        })?;
        if !output.status.success() {
            return Err(CreateBootimageError::BootloaderBuildFailed {
                stderr: output.stderr,
            });
        }
        let mut bootloader_elf_path = None;
        for line in String::from_utf8(output.stdout)
            .map_err(CreateBootimageError::BuildJsonOutputInvalidUtf8)?
            .lines()
        {
            let mut artifact =
                json::parse(line).map_err(CreateBootimageError::BuildJsonOutputInvalidJson)?;
            if let Some(executable) = artifact["executable"].take_string() {
                if bootloader_elf_path
                    .replace(PathBuf::from(executable))
                    .is_some()
                {
                    return Err(BootloaderError::BootloaderInvalid(
                        "bootloader has multiple executables".into(),
                    )
                    .into());
                }
            }
        }
        let bootloader_elf_path = bootloader_elf_path.ok_or_else(|| {
            BootloaderError::BootloaderInvalid("bootloader has no executable".into())
        })?;

        disk_image::create_disk_image(&bootloader_elf_path, output_bin_path)?;

        Ok(())
    }

    /// Returns the cargo metadata package that contains the given binary.
    pub fn kernel_package_for_bin(
        &mut self,
        kernel_bin_name: &str,
    ) -> Result<Option<&cargo_metadata::Package>, cargo_metadata::Error> {
        Ok(self.project_metadata()?.packages.iter().find(|p| {
            p.targets
                .iter()
                .any(|t| t.name == kernel_bin_name && t.kind.iter().any(|k| k == "bin"))
        }))
    }

    fn project_metadata(&mut self) -> Result<&Metadata, cargo_metadata::Error> {
        if let Some(ref metadata) = self.project_metadata {
            return Ok(metadata);
        }
        let metadata = cargo_metadata::MetadataCommand::new()
            .manifest_path(&self.manifest_path)
            .exec()?;
        Ok(self.project_metadata.get_or_insert(metadata))
    }
}