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
//  BUILD COMMON.rs
//    by Lut99
//
//  Created:
//    21 Feb 2022, 12:32:28
//  Last edited:
//    19 Apr 2023, 11:19:54
//  Auto updated?
//    Yes
//
//  Description:
//!   Contains common macros, constants and functions between the
//!   different
//

use std::fs;
use std::path::Path;
use std::process::Command;

use specifications::arch::Arch;

use crate::errors::BuildError;


/***** COMMON MACROS *****/
/// Wrapper around write! that returns BuildErrors instead of standard format errors.
macro_rules! write_build {
    ($($e:expr),*) => {
        write!($($e),*).map_err(|err| BuildError::DockerfileStrWriteError{ err })
    }
}

/// Wrapper around writeln! that returns BuildErrors instead of standard format errors.
macro_rules! writeln_build {
    ($($e:expr),*) => {
        writeln!($($e),*).map_err(|err| BuildError::DockerfileStrWriteError{ err })
    }
}





/***** COMMON CONSTANTS */
/// The URL which we use to pull the latest branelet executable from.
pub const BRANELET_URL: &str =
    concat!("https://github.com/epi-project/brane/releases/download/", concat!("v", env!("CARGO_PKG_VERSION")), "/branelet");





/***** COMMON FUNCTIONS *****/
/// **Edited: now returning BuildErrors. Also leaving .lock removal to the main handle function.**
///
/// Cleans the resulting build directory from the build files (but only if the build files should be removed).
///
/// **Arguments**
///  * `package_dir`: The directory to clean (we assume this has been canonicalized and thus exists).
///  * `files`: The files to remove from the build directory.
///
/// **Returns**  
/// Nothing - although this function will print BuildErrors as warnings to stderr using the logger.
pub fn clean_directory(package_dir: &Path, files: Vec<&str>) {
    // Remove the build files
    for file in files {
        let file = package_dir.join(file);
        if file.is_file() {
            if let Err(err) = fs::remove_file(&file) {
                warn!("{}", BuildError::FileCleanupError { path: file, err });
            }
        } else if file.is_dir() {
            if let Err(err) = fs::remove_dir_all(&file) {
                warn!("{}", BuildError::DirCleanupError { path: file, err });
            }
        } else {
            warn!("To-be-cleaned file '{}' is neither a file nor a directory", file.display());
        }
    }
}



/// Builds the docker image in the given package directory.
///
/// # Generic types
///  - `P`: The Path-like type of the container directory path.
///
/// # Arguments
///  - `arch`: The architecture for which to build this image.
///  - `package_dir`: The build directory for this image. We expect the actual image files to be under ./container.
///  - `tag`: Tag to give to the image so we can find it later (probably just `<package name>:<package version>`)
///
/// # Errors
/// This function fails if Buildx could not be test-ran, it could not run the Docker build command or the Docker build command did not return a successfull exit code.
pub fn build_docker_image<P: AsRef<Path>>(arch: Arch, package_dir: P, tag: String) -> Result<(), BuildError> {
    // Prepare the command to check for buildx (and launch the buildx image, presumably)
    let mut command = Command::new("docker");
    command.arg("buildx");
    let buildx = match command.output() {
        Ok(buildx) => buildx,
        Err(err) => {
            return Err(BuildError::BuildKitLaunchError { command: format!("{command:?}"), err });
        },
    };
    // Check if it was successfull
    if !buildx.status.success() {
        return Err(BuildError::BuildKitError {
            command: format!("{command:?}"),
            code:    buildx.status.code().unwrap_or(-1),
            stdout:  String::from_utf8_lossy(&buildx.stdout).to_string(),
            stderr:  String::from_utf8_lossy(&buildx.stdout).to_string(),
        });
    }

    // Next, launch the command to actually build the image
    let mut command = Command::new("docker");
    command.arg("buildx");
    command.arg("build");
    command.arg("--output");
    command.arg("type=docker,dest=image.tar");
    command.arg("--tag");
    command.arg(tag);
    command.arg("--platform");
    command.arg(format!("linux/{}", arch.docker()));
    command.arg("--build-arg");
    command.arg(format!("BRANELET_ARCH={}", arch.brane()));
    command.arg("--build-arg");
    command.arg(format!("JUICEFS_ARCH={}", arch.juicefs()));
    command.arg(".");
    command.current_dir(package_dir);
    let output = match command.status() {
        Ok(output) => output,
        Err(err) => {
            return Err(BuildError::ImageBuildLaunchError { command: format!("{command:?}"), err });
        },
    };
    // Check if it was successfull
    if !output.success() {
        return Err(BuildError::ImageBuildError { command: format!("{command:?}"), code: output.code().unwrap_or(-1) });
    }

    // Done! :D
    Ok(())
}