Skip to main content

hugo_build/
lib.rs

1use cpio_archive::CpioReader;
2use flate2::read::GzDecoder;
3use std::{
4    fs::File,
5    io::{Read, Write},
6    path::{Path, PathBuf},
7    process::{Command, Output},
8};
9use tar::Archive;
10
11#[cfg(test)]
12mod test;
13
14#[derive(Debug, Default, Clone)]
15pub struct HugoBuilder {
16    /// path to the hugo binary
17    binary: PathBuf,
18    /// source directory
19    input_path: Option<PathBuf>,
20    /// target directory
21    output_path: Option<PathBuf>,
22}
23
24#[cfg(target_os = "macos")]
25static ARCH: &str = "darwin-universal";
26#[cfg(target_os = "linux")]
27static ARCH: &str = "Linux-64bit";
28#[cfg(target_os = "windows")]
29static ARCH: &str = "windows-amd64";
30
31static VERSION: &str = std::env!("CARGO_PKG_VERSION");
32
33fn sanitize_version(version: &str) -> String {
34    if let Some(index) = version.find("-") {
35        version[..index].to_string()
36    } else {
37        version.to_string()
38    }
39}
40
41fn binary_filename() -> &'static str {
42    if cfg!(target_os = "windows") {
43        "hugo.exe"
44    } else {
45        "hugo"
46    }
47}
48
49#[cfg(not(target_os = "windows"))]
50fn fix_permissions(local_file: &File) {
51    //set permissions
52    use std::os::unix::prelude::PermissionsExt;
53    let permissions = std::fs::Permissions::from_mode(0o755);
54    local_file.set_permissions(permissions).unwrap();
55}
56
57/// initialises a hugo build
58///
59/// fetches the binary from github if required
60pub fn init() -> HugoBuilder {
61    let version = sanitize_version(VERSION);
62    // fetch binary from github
63    let url = match ARCH {
64        "darwin-universal" => format!(
65            "https://github.com/gohugoio/hugo/releases/download/v{version}/hugo_extended_{version}_darwin-universal.pkg"
66        ),
67        _ => format!(
68            "https://github.com/gohugoio/hugo/releases/download/v{version}/hugo_extended_{version}_{ARCH}.tar.gz"
69        ),
70    };
71    let out_dir = std::env::var("OUT_DIR").unwrap();
72    let out_path = Path::new(&out_dir);
73    let mut binary_name = out_path.join(binary_filename());
74
75    // check for already downloaded binary
76    let mut binary_exists = false;
77    let result = out_path.read_dir().expect("reading OUT_DIR");
78    let expected_binary_name = binary_filename();
79    for file in result {
80        let entry = file.unwrap();
81        if entry.file_name() == std::ffi::OsStr::new(expected_binary_name)
82            && entry
83                .file_type()
84                .map(|file_type| file_type.is_file())
85                .unwrap_or(false)
86        {
87            binary_exists = true;
88            binary_name = entry.path();
89        }
90    }
91    if !binary_exists {
92        if ARCH == "darwin-universal" {
93            binary_name = download_pkg(&url, out_path).unwrap();
94        } else {
95            binary_name = download_tar_gz(&url, out_path).unwrap();
96        }
97    }
98    HugoBuilder {
99        binary: binary_name,
100        ..Default::default()
101    }
102}
103
104fn download_tar_gz(url: &str, out_path: &Path) -> Result<PathBuf, std::io::Error> {
105    // download fresh binary
106    let result = reqwest::blocking::get(url).unwrap();
107    let bytes = result.bytes().expect("downloading the hugo binary failed");
108    let decompressor = GzDecoder::new(&bytes[..]);
109    let mut archive = Archive::new(decompressor);
110    let mut binary_name = PathBuf::new();
111    for entry in archive.entries().unwrap() {
112        let mut file = entry.unwrap();
113        let file_path = file.path().unwrap();
114        let is_binary = file_path.starts_with("hugo");
115        let target_file_name = out_path.join(&file_path);
116        let mut bytes: Vec<u8> = vec![];
117        _ = file.read_to_end(&mut bytes).unwrap();
118        let mut local_file = File::create(target_file_name.clone()).unwrap();
119        local_file.write_all(&bytes).unwrap();
120        if is_binary {
121            binary_name = target_file_name.clone();
122            #[cfg(not(target_os = "windows"))]
123            fix_permissions(&local_file);
124        }
125    }
126    Ok(binary_name)
127}
128
129fn download_pkg(url: &str, out_path: &Path) -> Result<PathBuf, std::io::Error> {
130    // download fresh binary
131    let result = reqwest::blocking::get(url).unwrap();
132    let bytes = result.bytes().expect("downloading the hugo binary failed");
133    let mut cursor = std::io::Cursor::new(bytes);
134    let mut archive = apple_xar::reader::XarReader::new(&mut cursor).unwrap();
135    let archive_bytes = archive.get_file_data_from_path("Payload").unwrap().unwrap();
136
137    let mut decompressor = GzDecoder::new(&archive_bytes[..]);
138    let mut bytes2: Vec<u8> = vec![];
139    decompressor.read_to_end(&mut bytes2).unwrap();
140    let mut c = std::io::Cursor::new(bytes2);
141    let mut reader = cpio_archive::odc::OdcReader::new(&mut c);
142    let mut binary_name = PathBuf::new();
143    loop {
144        let entry = reader.read_next().unwrap();
145        if let Some(x) = entry {
146            let name = x.name();
147            let file_path = Path::new(name);
148            let is_binary = file_path.starts_with("hugo") || file_path.ends_with("hugo");
149            let target_file_name = out_path.join(file_path);
150
151            if x.file_size() > 0 {
152                if let Some(parent) = target_file_name.parent() {
153                    std::fs::create_dir_all(parent).ok();
154                }
155                let mut limit = std::io::Read::take(&mut reader, x.file_size());
156                let mut out_file = File::create(&target_file_name).unwrap();
157                std::io::copy(&mut limit, &mut out_file).unwrap();
158                if is_binary {
159                    binary_name = target_file_name.clone();
160                    #[cfg(not(target_os = "windows"))]
161                    fix_permissions(&out_file);
162                }
163            }
164        } else {
165            break;
166        }
167    }
168    if binary_name.as_os_str().is_empty() {
169        Err(std::io::Error::other("No binary found"))
170    } else {
171        Ok(binary_name)
172    }
173}
174
175impl HugoBuilder {
176    /// defines source directory for the hugo build
177    pub fn with_input(self, path: PathBuf) -> HugoBuilder {
178        let mut cpy = self;
179        cpy.input_path = Some(path);
180        cpy
181    }
182    /// defines target directory for the hugo build
183    pub fn with_output(self, path: PathBuf) -> HugoBuilder {
184        let mut cpy = self;
185        cpy.output_path = Some(path);
186        cpy
187    }
188    pub fn build(self) -> Result<Output, std::io::Error> {
189        let base = std::env::var("CARGO_MANIFEST_DIR").unwrap();
190        let input = match self.input_path {
191            None => {
192                println!("cargo:warning=no input path set, using ./site");
193                Path::new(&base).join("site")
194            }
195            Some(val) => val,
196        };
197        let output = match self.output_path {
198            None => {
199                println!("cargo:warning=no output path set, using ./target/site");
200                Path::new(&base).join("target").join("site")
201            }
202            Some(val) => val,
203        };
204        Command::new(self.binary)
205            .arg("-s")
206            .arg(input)
207            .arg("-d")
208            .arg(output)
209            .output()
210    }
211}