Update edition and rustfmt.
This commit is contained in:
@@ -6,6 +6,7 @@ license = "MIT"
|
||||
authors = ["Jeremy Soller <jackpot51@gmail.com>"]
|
||||
repository = "https://gitlab.redox-os.org/redox-os/installer"
|
||||
default-run = "redox_installer"
|
||||
edition = "2018"
|
||||
|
||||
[[bin]]
|
||||
name = "redox_installer"
|
||||
|
||||
+133
-92
@@ -1,5 +1,6 @@
|
||||
extern crate arg_parser;
|
||||
#[macro_use] extern crate failure;
|
||||
#[macro_use]
|
||||
extern crate failure;
|
||||
extern crate pkgar;
|
||||
extern crate pkgar_core;
|
||||
extern crate pkgar_keys;
|
||||
@@ -8,17 +9,17 @@ extern crate serde;
|
||||
extern crate termion;
|
||||
extern crate toml;
|
||||
|
||||
use pkgar::{PackageHead, ext::EntryExt};
|
||||
use pkgar::{ext::EntryExt, PackageHead};
|
||||
use pkgar_core::PackageSrc;
|
||||
use pkgar_keys::PublicKeyFile;
|
||||
use redox_installer::{Config, with_whole_disk, DiskOption};
|
||||
use redox_installer::{with_whole_disk, Config, DiskOption};
|
||||
use std::{
|
||||
ffi::OsStr,
|
||||
fs,
|
||||
io::{self, Read, Write},
|
||||
os::unix::fs::{MetadataExt, OpenOptionsExt, symlink},
|
||||
os::unix::fs::{symlink, MetadataExt, OpenOptionsExt},
|
||||
path::Path,
|
||||
process
|
||||
process,
|
||||
};
|
||||
use termion::input::TermRead;
|
||||
|
||||
@@ -29,52 +30,54 @@ fn disk_paths(_paths: &mut Vec<(String, u64)>) {}
|
||||
fn disk_paths(paths: &mut Vec<(String, u64)>) {
|
||||
let mut schemes = Vec::new();
|
||||
match fs::read_dir("/scheme") {
|
||||
Ok(entries) => for entry_res in entries {
|
||||
if let Ok(entry) = entry_res {
|
||||
let path = entry.path();
|
||||
if let Ok(path_str) = path.into_os_string().into_string() {
|
||||
let scheme = path_str.trim_start_matches(':').trim_matches('/');
|
||||
if scheme.starts_with("disk") {
|
||||
if scheme == "disk/live" {
|
||||
// Skip live disks
|
||||
continue;
|
||||
}
|
||||
Ok(entries) => {
|
||||
for entry_res in entries {
|
||||
if let Ok(entry) = entry_res {
|
||||
let path = entry.path();
|
||||
if let Ok(path_str) = path.into_os_string().into_string() {
|
||||
let scheme = path_str.trim_start_matches(':').trim_matches('/');
|
||||
if scheme.starts_with("disk") {
|
||||
if scheme == "disk/live" {
|
||||
// Skip live disks
|
||||
continue;
|
||||
}
|
||||
|
||||
schemes.push(format!("{}:", scheme));
|
||||
schemes.push(format!("{}:", scheme));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
Err(err) => {
|
||||
eprintln!("installer_tui: failed to list schemes: {}", err);
|
||||
}
|
||||
}
|
||||
|
||||
for scheme in schemes {
|
||||
let is_dir = fs::metadata(&scheme)
|
||||
.map(|x| x.is_dir())
|
||||
.unwrap_or(false);
|
||||
let is_dir = fs::metadata(&scheme).map(|x| x.is_dir()).unwrap_or(false);
|
||||
if is_dir {
|
||||
match fs::read_dir(&scheme) {
|
||||
Ok(entries) => for entry_res in entries {
|
||||
if let Ok(entry) = entry_res {
|
||||
if let Ok(file_name) = entry.file_name().into_string() {
|
||||
if file_name.contains('p') {
|
||||
// Skip partitions
|
||||
continue;
|
||||
}
|
||||
Ok(entries) => {
|
||||
for entry_res in entries {
|
||||
if let Ok(entry) = entry_res {
|
||||
if let Ok(file_name) = entry.file_name().into_string() {
|
||||
if file_name.contains('p') {
|
||||
// Skip partitions
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Ok(path) = entry.path().into_os_string().into_string() {
|
||||
if let Ok(metadata) = entry.metadata() {
|
||||
let size = metadata.len();
|
||||
if size > 0 {
|
||||
paths.push((path, size));
|
||||
if let Ok(path) = entry.path().into_os_string().into_string() {
|
||||
if let Ok(metadata) = entry.metadata() {
|
||||
let size = metadata.len();
|
||||
if size > 0 {
|
||||
paths.push((path, size));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
Err(err) => {
|
||||
eprintln!("installer_tui: failed to list '{}': {}", scheme, err);
|
||||
}
|
||||
@@ -105,11 +108,15 @@ fn format_size(size: u64) -> String {
|
||||
fn copy_file(src: &Path, dest: &Path, buf: &mut [u8]) -> Result<(), failure::Error> {
|
||||
if let Some(parent) = dest.parent() {
|
||||
// Parent may be a symlink
|
||||
if ! parent.is_symlink() {
|
||||
if !parent.is_symlink() {
|
||||
match fs::create_dir_all(&parent) {
|
||||
Ok(()) => (),
|
||||
Err(err) => {
|
||||
return Err(format_err!("failed to create directory {}: {}", parent.display(), err));
|
||||
return Err(format_err!(
|
||||
"failed to create directory {}: {}",
|
||||
parent.display(),
|
||||
err
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -118,36 +125,63 @@ fn copy_file(src: &Path, dest: &Path, buf: &mut [u8]) -> Result<(), failure::Err
|
||||
let metadata = match fs::symlink_metadata(&src) {
|
||||
Ok(ok) => ok,
|
||||
Err(err) => {
|
||||
return Err(format_err!("failed to read metadata of {}: {}", src.display(), err));
|
||||
},
|
||||
return Err(format_err!(
|
||||
"failed to read metadata of {}: {}",
|
||||
src.display(),
|
||||
err
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
if metadata.file_type().is_symlink() {
|
||||
let real_src = match fs::read_link(&src) {
|
||||
Ok(ok) => ok,
|
||||
Err(err) => {
|
||||
return Err(format_err!("failed to read link {}: {}", src.display(), err));
|
||||
return Err(format_err!(
|
||||
"failed to read link {}: {}",
|
||||
src.display(),
|
||||
err
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
match symlink(&real_src, &dest) {
|
||||
Ok(()) => (),
|
||||
Err(err) => {
|
||||
return Err(format_err!("failed to copy link {} ({}) to {}: {}", src.display(), real_src.display(), dest.display(), err));
|
||||
},
|
||||
return Err(format_err!(
|
||||
"failed to copy link {} ({}) to {}: {}",
|
||||
src.display(),
|
||||
real_src.display(),
|
||||
dest.display(),
|
||||
err
|
||||
));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let mut src_file = match fs::File::open(&src) {
|
||||
Ok(ok) => ok,
|
||||
Err(err) => {
|
||||
return Err(format_err!("failed to open file {}: {}", src.display(), err));
|
||||
return Err(format_err!(
|
||||
"failed to open file {}: {}",
|
||||
src.display(),
|
||||
err
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
let mut dest_file = match fs::OpenOptions::new().write(true).create_new(true).mode(metadata.mode()).open(&dest) {
|
||||
let mut dest_file = match fs::OpenOptions::new()
|
||||
.write(true)
|
||||
.create_new(true)
|
||||
.mode(metadata.mode())
|
||||
.open(&dest)
|
||||
{
|
||||
Ok(ok) => ok,
|
||||
Err(err) => {
|
||||
return Err(format_err!("failed to create file {}: {}", dest.display(), err));
|
||||
return Err(format_err!(
|
||||
"failed to create file {}: {}",
|
||||
dest.display(),
|
||||
err
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -155,7 +189,11 @@ fn copy_file(src: &Path, dest: &Path, buf: &mut [u8]) -> Result<(), failure::Err
|
||||
let count = match src_file.read(buf) {
|
||||
Ok(ok) => ok,
|
||||
Err(err) => {
|
||||
return Err(format_err!("failed to read file {}: {}", src.display(), err));
|
||||
return Err(format_err!(
|
||||
"failed to read file {}: {}",
|
||||
src.display(),
|
||||
err
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -166,7 +204,11 @@ fn copy_file(src: &Path, dest: &Path, buf: &mut [u8]) -> Result<(), failure::Err
|
||||
match dest_file.write_all(&buf[..count]) {
|
||||
Ok(()) => (),
|
||||
Err(err) => {
|
||||
return Err(format_err!("failed to write file {}: {}", dest.display(), err));
|
||||
return Err(format_err!(
|
||||
"failed to write file {}: {}",
|
||||
dest.display(),
|
||||
err
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -175,7 +217,11 @@ fn copy_file(src: &Path, dest: &Path, buf: &mut [u8]) -> Result<(), failure::Err
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn package_files(root_path: &Path, config: &mut Config, files: &mut Vec<String>) -> Result<(), pkgar::Error> {
|
||||
fn package_files(
|
||||
root_path: &Path,
|
||||
config: &mut Config,
|
||||
files: &mut Vec<String>,
|
||||
) -> Result<(), pkgar::Error> {
|
||||
//TODO: Remove packages from config where all files are located (and have valid shasum?)
|
||||
config.packages.clear();
|
||||
|
||||
@@ -189,18 +235,15 @@ fn package_files(root_path: &Path, config: &mut Config, files: &mut Vec<String>)
|
||||
if pkg_path.extension() == Some(OsStr::new("pkgar_head")) {
|
||||
let mut pkg = PackageHead::new(&pkg_path, &root_path, &pkey)?;
|
||||
for entry in pkg.read_entries()? {
|
||||
files.push(
|
||||
entry
|
||||
.check_path()?
|
||||
.to_str().unwrap()
|
||||
.to_string()
|
||||
);
|
||||
files.push(entry.check_path()?.to_str().unwrap().to_string());
|
||||
}
|
||||
files.push(
|
||||
pkg_path
|
||||
.strip_prefix(root_path).unwrap()
|
||||
.to_str().unwrap()
|
||||
.to_string()
|
||||
.strip_prefix(root_path)
|
||||
.unwrap()
|
||||
.to_str()
|
||||
.unwrap()
|
||||
.to_string(),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -227,7 +270,7 @@ fn choose_disk() -> String {
|
||||
Ok(0) => {
|
||||
eprintln!("installer_tui: failed to read line: end of input");
|
||||
process::exit(1);
|
||||
},
|
||||
}
|
||||
Ok(_) => (),
|
||||
Err(err) => {
|
||||
eprintln!("installer_tui: failed to read line: {}", err);
|
||||
@@ -242,7 +285,7 @@ fn choose_disk() -> String {
|
||||
} else {
|
||||
eprintln!("{} not from 1 to {}", i, paths.len());
|
||||
}
|
||||
},
|
||||
}
|
||||
Err(err) => {
|
||||
eprintln!("invalid input: {}", err);
|
||||
}
|
||||
@@ -262,7 +305,7 @@ fn choose_password() -> Option<String> {
|
||||
eprintln!();
|
||||
|
||||
if password.is_empty() {
|
||||
return None;
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(password)
|
||||
@@ -311,53 +354,51 @@ fn main() {
|
||||
password_opt: password_opt.as_ref().map(|x| x.as_bytes()),
|
||||
efi_partition_size: None,
|
||||
};
|
||||
let res = with_whole_disk(&disk_path, &disk_option, |mount_path| -> Result<(), failure::Error> {
|
||||
let mut config: Config = Config::from_file(&root_path.join("filesystem.toml"))?;
|
||||
let res = with_whole_disk(
|
||||
&disk_path,
|
||||
&disk_option,
|
||||
|mount_path| -> Result<(), failure::Error> {
|
||||
let mut config: Config = Config::from_file(&root_path.join("filesystem.toml"))?;
|
||||
|
||||
// Copy filesystem.toml, which is not packaged
|
||||
let mut files = vec![
|
||||
"filesystem.toml".to_string(),
|
||||
];
|
||||
// Copy filesystem.toml, which is not packaged
|
||||
let mut files = vec!["filesystem.toml".to_string()];
|
||||
|
||||
// Copy files from locally installed packages
|
||||
if let Err(err) = package_files(&root_path, &mut config, &mut files) {
|
||||
return Err(format_err!("failed to read package files: {}", err));
|
||||
}
|
||||
// Copy files from locally installed packages
|
||||
if let Err(err) = package_files(&root_path, &mut config, &mut files) {
|
||||
return Err(format_err!("failed to read package files: {}", err));
|
||||
}
|
||||
|
||||
// Perform config install (after packages have been converted to files)
|
||||
eprintln!("configuring system");
|
||||
let cookbook: Option<&'static str> = None;
|
||||
redox_installer::install_dir(config, mount_path, cookbook).map_err(|err| {
|
||||
io::Error::new(
|
||||
io::ErrorKind::Other,
|
||||
err
|
||||
)
|
||||
})?;
|
||||
// Perform config install (after packages have been converted to files)
|
||||
eprintln!("configuring system");
|
||||
let cookbook: Option<&'static str> = None;
|
||||
redox_installer::install_dir(config, mount_path, cookbook)
|
||||
.map_err(|err| io::Error::new(io::ErrorKind::Other, err))?;
|
||||
|
||||
// Sort and remove duplicates
|
||||
files.sort();
|
||||
files.dedup();
|
||||
// Sort and remove duplicates
|
||||
files.sort();
|
||||
files.dedup();
|
||||
|
||||
// Install files
|
||||
let mut buf = vec![0; 4 * MIB as usize];
|
||||
for (i, name) in files.iter().enumerate() {
|
||||
eprintln!("copy {} [{}/{}]", name, i, files.len());
|
||||
// Install files
|
||||
let mut buf = vec![0; 4 * MIB as usize];
|
||||
for (i, name) in files.iter().enumerate() {
|
||||
eprintln!("copy {} [{}/{}]", name, i, files.len());
|
||||
|
||||
let src = root_path.join(name);
|
||||
let dest = mount_path.join(name);
|
||||
copy_file(&src, &dest, &mut buf)?;
|
||||
}
|
||||
let src = root_path.join(name);
|
||||
let dest = mount_path.join(name);
|
||||
copy_file(&src, &dest, &mut buf)?;
|
||||
}
|
||||
|
||||
eprintln!("finished installing, unmounting filesystem");
|
||||
eprintln!("finished installing, unmounting filesystem");
|
||||
|
||||
Ok(())
|
||||
});
|
||||
Ok(())
|
||||
},
|
||||
);
|
||||
|
||||
match res {
|
||||
Ok(()) => {
|
||||
eprintln!("installer_tui: installed successfully");
|
||||
process::exit(0);
|
||||
},
|
||||
}
|
||||
Err(err) => {
|
||||
eprintln!("installer_tui: failed to install: {}", err);
|
||||
process::exit(1);
|
||||
|
||||
+7
-11
@@ -1,12 +1,11 @@
|
||||
|
||||
use Result;
|
||||
use crate::Result;
|
||||
use libc::{gid_t, uid_t};
|
||||
|
||||
use std::io::{Error, Write};
|
||||
use std::ffi::{CString, OsStr};
|
||||
use std::fs::{self, File};
|
||||
use std::io::{Error, Write};
|
||||
use std::os::unix::ffi::OsStrExt;
|
||||
use std::os::unix::fs::{PermissionsExt, symlink};
|
||||
use std::os::unix::fs::{symlink, PermissionsExt};
|
||||
use std::path::Path;
|
||||
|
||||
//type Result<T> = std::result::Result<T, Error>;
|
||||
@@ -48,8 +47,7 @@ pub struct FileConfig {
|
||||
impl FileConfig {
|
||||
pub(crate) fn create<P: AsRef<Path>>(&self, prefix: P) -> Result<()> {
|
||||
let path = self.path.trim_start_matches('/');
|
||||
let target_file = prefix.as_ref()
|
||||
.join(path);
|
||||
let target_file = prefix.as_ref().join(path);
|
||||
|
||||
if self.directory {
|
||||
println!("Create directory {}", target_file.display());
|
||||
@@ -76,11 +74,9 @@ impl FileConfig {
|
||||
|
||||
fn apply_perms<P: AsRef<Path>>(&self, target: P) -> Result<()> {
|
||||
let path = target.as_ref();
|
||||
let mode = self.mode.unwrap_or_else(|| if self.directory {
|
||||
0o0755
|
||||
} else {
|
||||
0o0644
|
||||
});
|
||||
let mode = self
|
||||
.mode
|
||||
.unwrap_or_else(|| if self.directory { 0o0755 } else { 0o0644 });
|
||||
let uid = self.uid.unwrap_or(!0);
|
||||
let gid = self.gid.unwrap_or(!0);
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ pub struct GeneralConfig {
|
||||
pub prompt: Option<bool>,
|
||||
// Allow config to specify cookbook recipe or binary package as default
|
||||
pub repo_binary: Option<bool>,
|
||||
pub filesystem_size: Option<u32>, //MiB
|
||||
pub filesystem_size: Option<u32>, //MiB
|
||||
pub efi_partition_size: Option<u32>, //MiB
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ pub enum PackageConfig {
|
||||
version: Option<String>,
|
||||
git: Option<String>,
|
||||
path: Option<String>,
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
impl Default for PackageConfig {
|
||||
|
||||
+16
-20
@@ -21,10 +21,7 @@ enum Buffer<'a> {
|
||||
|
||||
impl DiskWrapper {
|
||||
pub fn open<P: AsRef<Path>>(path: P) -> Result<Self> {
|
||||
let disk = OpenOptions::new()
|
||||
.read(true)
|
||||
.write(true)
|
||||
.open(path)?;
|
||||
let disk = OpenOptions::new().read(true).write(true).open(path)?;
|
||||
let metadata = disk.metadata()?;
|
||||
let size = metadata.len();
|
||||
// TODO: get real block size: disk_metadata.blksize() works on disks but not image files
|
||||
@@ -71,24 +68,27 @@ impl DiskWrapper {
|
||||
let remaining = buf_len.checked_sub(i).unwrap();
|
||||
let len = cmp::min(
|
||||
remaining,
|
||||
self.block.len().checked_sub(offset.try_into().unwrap()).unwrap()
|
||||
self.block
|
||||
.len()
|
||||
.checked_sub(offset.try_into().unwrap())
|
||||
.unwrap(),
|
||||
);
|
||||
|
||||
self.disk.seek(SeekFrom::Start(block.checked_mul(block_len).unwrap()))?;
|
||||
self.disk
|
||||
.seek(SeekFrom::Start(block.checked_mul(block_len).unwrap()))?;
|
||||
self.disk.read_exact(&mut self.block)?;
|
||||
|
||||
match buf {
|
||||
Buffer::Read(read) => {
|
||||
read[i..i.checked_add(len).unwrap()].copy_from_slice(
|
||||
&self.block[offset..offset.checked_add(len).unwrap()]
|
||||
);
|
||||
},
|
||||
read[i..i.checked_add(len).unwrap()]
|
||||
.copy_from_slice(&self.block[offset..offset.checked_add(len).unwrap()]);
|
||||
}
|
||||
Buffer::Write(write) => {
|
||||
self.block[offset..offset.checked_add(len).unwrap()].copy_from_slice(
|
||||
&write[i..i.checked_add(len).unwrap()]
|
||||
);
|
||||
self.block[offset..offset.checked_add(len).unwrap()]
|
||||
.copy_from_slice(&write[i..i.checked_add(len).unwrap()]);
|
||||
|
||||
self.disk.seek(SeekFrom::Start(block.checked_mul(block_len).unwrap()))?;
|
||||
self.disk
|
||||
.seek(SeekFrom::Start(block.checked_mul(block_len).unwrap()))?;
|
||||
self.disk.write_all(&mut self.block)?;
|
||||
}
|
||||
}
|
||||
@@ -112,12 +112,8 @@ impl Seek for DiskWrapper {
|
||||
let current: i64 = self.seek.try_into().unwrap();
|
||||
let end: i64 = self.size.try_into().unwrap();
|
||||
self.seek = match pos {
|
||||
SeekFrom::Start(offset) => {
|
||||
cmp::min(self.size, offset)
|
||||
},
|
||||
SeekFrom::End(offset) => {
|
||||
cmp::max(0, cmp::min(end, end.wrapping_add(offset))) as u64
|
||||
},
|
||||
SeekFrom::Start(offset) => cmp::min(self.size, offset),
|
||||
SeekFrom::End(offset) => cmp::max(0, cmp::min(end, end.wrapping_add(offset))) as u64,
|
||||
SeekFrom::Current(offset) => {
|
||||
cmp::max(0, cmp::min(end, current.wrapping_add(offset))) as u64
|
||||
}
|
||||
|
||||
+201
-136
@@ -14,27 +14,26 @@ extern crate termion;
|
||||
mod config;
|
||||
mod disk_wrapper;
|
||||
|
||||
pub use config::Config;
|
||||
pub use config::file::FileConfig;
|
||||
pub use config::package::PackageConfig;
|
||||
use disk_wrapper::DiskWrapper;
|
||||
pub use crate::config::file::FileConfig;
|
||||
pub use crate::config::package::PackageConfig;
|
||||
pub use crate::config::Config;
|
||||
use crate::disk_wrapper::DiskWrapper;
|
||||
|
||||
use failure::{Error, err_msg};
|
||||
use rand::{RngCore, rngs::OsRng};
|
||||
use failure::{err_msg, Error};
|
||||
use pkgutils::{Package, Repo};
|
||||
use rand::{rngs::OsRng, RngCore};
|
||||
use redoxfs::{unmount_path, Disk, DiskIo, FileSystem};
|
||||
use termion::input::TermRead;
|
||||
use pkgutils::{Repo, Package};
|
||||
|
||||
use std::{
|
||||
collections::BTreeMap,
|
||||
env,
|
||||
fs,
|
||||
env, fs,
|
||||
io::{self, Seek, SeekFrom, Write},
|
||||
path::Path,
|
||||
process,
|
||||
sync::mpsc::channel,
|
||||
time::{SystemTime, UNIX_EPOCH},
|
||||
thread,
|
||||
time::{SystemTime, UNIX_EPOCH},
|
||||
};
|
||||
|
||||
pub(crate) type Result<T> = std::result::Result<T, Error>;
|
||||
@@ -50,10 +49,7 @@ const REMOTE: &'static str = "https://static.redox-os.org/pkg";
|
||||
|
||||
fn get_target() -> String {
|
||||
env::var("TARGET").unwrap_or(
|
||||
option_env!("TARGET").map_or(
|
||||
"x86_64-unknown-redox".to_string(),
|
||||
|x| x.to_string()
|
||||
)
|
||||
option_env!("TARGET").map_or("x86_64-unknown-redox".to_string(), |x| x.to_string()),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -104,37 +100,54 @@ fn install_packages<S: AsRef<str>>(config: &Config, dest: &str, cookbook: Option
|
||||
|
||||
if let Some(cookbook) = cookbook {
|
||||
let dest_pkg = format!("{}/pkg", dest);
|
||||
if ! Path::new(&dest_pkg).exists() {
|
||||
if !Path::new(&dest_pkg).exists() {
|
||||
fs::create_dir(&dest_pkg).unwrap();
|
||||
}
|
||||
|
||||
for (packagename, package) in &config.packages {
|
||||
let pkgar_path = format!("{}/{}/repo/{}/{}.pkgar",
|
||||
env::current_dir().unwrap().to_string_lossy(),
|
||||
cookbook.as_ref(), target, packagename);
|
||||
let pkgar_path = format!(
|
||||
"{}/{}/repo/{}/{}.pkgar",
|
||||
env::current_dir().unwrap().to_string_lossy(),
|
||||
cookbook.as_ref(),
|
||||
target,
|
||||
packagename
|
||||
);
|
||||
let from_remote = match (config.general.repo_binary, package) {
|
||||
(Some(true), PackageConfig::Empty) => true,
|
||||
(Some(true), PackageConfig::Spec { version: None, git: None, path: None }) => true,
|
||||
(
|
||||
Some(true),
|
||||
PackageConfig::Spec {
|
||||
version: None,
|
||||
git: None,
|
||||
path: None,
|
||||
},
|
||||
) => true,
|
||||
(_, PackageConfig::Build(rule)) if rule == "binary" => true,
|
||||
_ => false
|
||||
_ => false,
|
||||
};
|
||||
if from_remote {
|
||||
println!("Installing package from remote: {}", packagename);
|
||||
repo.fetch(&packagename).unwrap().install(dest).unwrap();
|
||||
} else if Path::new(&pkgar_path).exists() {
|
||||
println!("Installing package from local repo: {}", packagename);
|
||||
let public_path = format!("{}/{}/build/id_ed25519.pub.toml",
|
||||
env::current_dir().unwrap().to_string_lossy(),
|
||||
cookbook.as_ref());
|
||||
let public_path = format!(
|
||||
"{}/{}/build/id_ed25519.pub.toml",
|
||||
env::current_dir().unwrap().to_string_lossy(),
|
||||
cookbook.as_ref()
|
||||
);
|
||||
pkgar::extract(&public_path, &pkgar_path, dest).unwrap();
|
||||
|
||||
let head_path = format!("{}/{}.pkgar_head", dest_pkg, packagename);
|
||||
pkgar::split(&public_path, &pkgar_path, &head_path, Option::<&str>::None).unwrap();
|
||||
} else {
|
||||
println!("Installing package tar.gz from local repo: {}", packagename);
|
||||
let path = format!("{}/{}/repo/{}/{}.tar.gz",
|
||||
env::current_dir().unwrap().to_string_lossy(),
|
||||
cookbook.as_ref(), target, packagename);
|
||||
let path = format!(
|
||||
"{}/{}/repo/{}/{}.tar.gz",
|
||||
env::current_dir().unwrap().to_string_lossy(),
|
||||
cookbook.as_ref(),
|
||||
target,
|
||||
packagename
|
||||
);
|
||||
Package::from_path(&path).unwrap().install(dest).unwrap();
|
||||
}
|
||||
}
|
||||
@@ -146,26 +159,32 @@ fn install_packages<S: AsRef<str>>(config: &Config, dest: &str, cookbook: Option
|
||||
}
|
||||
}
|
||||
|
||||
pub fn install_dir<P: AsRef<Path>, S: AsRef<str>>(config: Config, output_dir: P, cookbook: Option<S>) -> Result<()> {
|
||||
pub fn install_dir<P: AsRef<Path>, S: AsRef<str>>(
|
||||
config: Config,
|
||||
output_dir: P,
|
||||
cookbook: Option<S>,
|
||||
) -> Result<()> {
|
||||
//let mut context = liner::Context::new();
|
||||
|
||||
macro_rules! prompt {
|
||||
($dst:expr, $def:expr, $($arg:tt)*) => (if config.general.prompt.unwrap_or(true) {
|
||||
Err(io::Error::new(
|
||||
io::ErrorKind::Other,
|
||||
"prompt not currently supported"
|
||||
))
|
||||
// match unwrap_or_prompt($dst, &mut context, &format!($($arg)*)) {
|
||||
// Ok(res) => if res.is_empty() {
|
||||
// Ok($def)
|
||||
// } else {
|
||||
// Ok(res)
|
||||
// },
|
||||
// Err(err) => Err(err)
|
||||
// }
|
||||
} else {
|
||||
Ok($dst.unwrap_or($def))
|
||||
})
|
||||
($dst:expr, $def:expr, $($arg:tt)*) => {
|
||||
if config.general.prompt.unwrap_or(true) {
|
||||
Err(io::Error::new(
|
||||
io::ErrorKind::Other,
|
||||
"prompt not currently supported",
|
||||
))
|
||||
// match unwrap_or_prompt($dst, &mut context, &format!($($arg)*)) {
|
||||
// Ok(res) => if res.is_empty() {
|
||||
// Ok($def)
|
||||
// } else {
|
||||
// Ok(res)
|
||||
// },
|
||||
// Err(err) => Err(err)
|
||||
// }
|
||||
} else {
|
||||
Ok($dst.unwrap_or($def))
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
let output_dir = output_dir.as_ref();
|
||||
@@ -192,7 +211,8 @@ pub fn install_dir<P: AsRef<Path>, S: AsRef<str>>(config: Config, output_dir: P,
|
||||
} else if config.general.prompt.unwrap_or(true) {
|
||||
prompt_password(
|
||||
&format!("{}: enter password: ", username),
|
||||
&format!("{}: confirm password: ", username))?
|
||||
&format!("{}: confirm password: ", username),
|
||||
)?
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
@@ -209,9 +229,26 @@ pub fn install_dir<P: AsRef<Path>, S: AsRef<str>>(config: Config, output_dir: P,
|
||||
next_gid = gid + 1;
|
||||
}
|
||||
|
||||
let name = prompt!(user.name, username.clone(), "{}: name (GECOS) [{}]: ", username, username)?;
|
||||
let home = prompt!(user.home, format!("/home/{}", username), "{}: home [/home/{}]: ", username, username)?;
|
||||
let shell = prompt!(user.shell, "/bin/ion".to_string(), "{}: shell [/bin/ion]: ", username)?;
|
||||
let name = prompt!(
|
||||
user.name,
|
||||
username.clone(),
|
||||
"{}: name (GECOS) [{}]: ",
|
||||
username,
|
||||
username
|
||||
)?;
|
||||
let home = prompt!(
|
||||
user.home,
|
||||
format!("/home/{}", username),
|
||||
"{}: home [/home/{}]: ",
|
||||
username,
|
||||
username
|
||||
)?;
|
||||
let shell = prompt!(
|
||||
user.shell,
|
||||
"/bin/ion".to_string(),
|
||||
"{}: shell [/bin/ion]: ",
|
||||
username
|
||||
)?;
|
||||
|
||||
println!("Adding user {}:", username);
|
||||
println!("\tPassword: {}", password);
|
||||
@@ -230,7 +267,8 @@ pub fn install_dir<P: AsRef<Path>, S: AsRef<str>>(config: Config, output_dir: P,
|
||||
uid: Some(uid),
|
||||
gid: Some(gid),
|
||||
recursive_chown: true,
|
||||
}.create(&output_dir)?;
|
||||
}
|
||||
.create(&output_dir)?;
|
||||
|
||||
if uid >= 1000 {
|
||||
// Create XDG user dirs
|
||||
@@ -259,7 +297,8 @@ pub fn install_dir<P: AsRef<Path>, S: AsRef<str>>(config: Config, output_dir: P,
|
||||
uid: Some(uid),
|
||||
gid: Some(gid),
|
||||
recursive_chown: false,
|
||||
}.create(&output_dir)?;
|
||||
}
|
||||
.create(&output_dir)?;
|
||||
}
|
||||
FileConfig {
|
||||
path: format!("{}/.config/user-dirs.dirs", home),
|
||||
@@ -272,19 +311,24 @@ XDG_PICTURES_DIR="$HOME/Pictures"
|
||||
XDG_PUBLICSHARE_DIR="$HOME/Public"
|
||||
XDG_TEMPLATES_DIR="$HOME/Templates"
|
||||
XDG_VIDEOS_DIR="$HOME/Videos"
|
||||
"#.to_string(),
|
||||
"#
|
||||
.to_string(),
|
||||
symlink: false,
|
||||
directory: false,
|
||||
mode: Some(0o0600),
|
||||
uid: Some(uid),
|
||||
gid: Some(gid),
|
||||
recursive_chown: false,
|
||||
}.create(&output_dir)?;
|
||||
}
|
||||
.create(&output_dir)?;
|
||||
}
|
||||
|
||||
let password = hash_password(&password)?;
|
||||
|
||||
passwd.push_str(&format!("{};{};{};{};{};{}\n", username, uid, gid, name, home, shell));
|
||||
passwd.push_str(&format!(
|
||||
"{};{};{};{};{};{}\n",
|
||||
username, uid, gid, name, home, shell
|
||||
));
|
||||
shadow.push_str(&format!("{};{}\n", username, password));
|
||||
groups.push((username.clone(), gid, vec![username]));
|
||||
}
|
||||
@@ -312,7 +356,8 @@ XDG_VIDEOS_DIR="$HOME/Videos"
|
||||
uid: None,
|
||||
gid: None,
|
||||
recursive_chown: false,
|
||||
}.create(&output_dir)?;
|
||||
}
|
||||
.create(&output_dir)?;
|
||||
}
|
||||
|
||||
if !shadow.is_empty() {
|
||||
@@ -325,7 +370,8 @@ XDG_VIDEOS_DIR="$HOME/Videos"
|
||||
uid: Some(0),
|
||||
gid: Some(0),
|
||||
recursive_chown: false,
|
||||
}.create(&output_dir)?;
|
||||
}
|
||||
.create(&output_dir)?;
|
||||
}
|
||||
|
||||
if !groups.is_empty() {
|
||||
@@ -350,16 +396,17 @@ XDG_VIDEOS_DIR="$HOME/Videos"
|
||||
uid: None,
|
||||
gid: None,
|
||||
recursive_chown: false,
|
||||
}.create(&output_dir)?;
|
||||
}
|
||||
.create(&output_dir)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn with_redoxfs<D, T, F>(disk: D, password_opt: Option<&[u8]>, callback: F)
|
||||
-> Result<T> where
|
||||
D: Disk + Send + 'static,
|
||||
F: FnOnce(&Path) -> Result<T>
|
||||
pub fn with_redoxfs<D, T, F>(disk: D, password_opt: Option<&[u8]>, callback: F) -> Result<T>
|
||||
where
|
||||
D: Disk + Send + 'static,
|
||||
F: FnOnce(&Path) -> Result<T>,
|
||||
{
|
||||
let mount_path = if cfg!(target_os = "redox") {
|
||||
format!("file.redox_installer_{}", process::id())
|
||||
@@ -368,35 +415,27 @@ pub fn with_redoxfs<D, T, F>(disk: D, password_opt: Option<&[u8]>, callback: F)
|
||||
};
|
||||
|
||||
if cfg!(not(target_os = "redox")) {
|
||||
if ! Path::new(&mount_path).exists() {
|
||||
if !Path::new(&mount_path).exists() {
|
||||
fs::create_dir(&mount_path)?;
|
||||
}
|
||||
}
|
||||
|
||||
let ctime = SystemTime::now().duration_since(UNIX_EPOCH)?;
|
||||
let fs = FileSystem::create(
|
||||
disk,
|
||||
password_opt,
|
||||
ctime.as_secs(),
|
||||
ctime.subsec_nanos()
|
||||
).map_err(syscall_error)?;
|
||||
let fs = FileSystem::create(disk, password_opt, ctime.as_secs(), ctime.subsec_nanos())
|
||||
.map_err(syscall_error)?;
|
||||
|
||||
let (tx, rx) = channel();
|
||||
let join_handle = {
|
||||
let mount_path = mount_path.clone();
|
||||
thread::spawn(move || {
|
||||
let res = redoxfs::mount(
|
||||
fs,
|
||||
&mount_path,
|
||||
|real_path| {
|
||||
tx.send(Ok(real_path.to_owned())).unwrap();
|
||||
}
|
||||
);
|
||||
let res = redoxfs::mount(fs, &mount_path, |real_path| {
|
||||
tx.send(Ok(real_path.to_owned())).unwrap();
|
||||
});
|
||||
match res {
|
||||
Ok(()) => (),
|
||||
Err(err) => {
|
||||
tx.send(Err(err)).unwrap();
|
||||
},
|
||||
}
|
||||
};
|
||||
})
|
||||
};
|
||||
@@ -406,10 +445,13 @@ pub fn with_redoxfs<D, T, F>(disk: D, password_opt: Option<&[u8]>, callback: F)
|
||||
Ok(real_path) => callback(&real_path),
|
||||
Err(err) => return Err(err.into()),
|
||||
},
|
||||
Err(_) => return Err(io::Error::new(
|
||||
io::ErrorKind::NotConnected,
|
||||
"redoxfs thread did not send a result"
|
||||
).into()),
|
||||
Err(_) => {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::NotConnected,
|
||||
"redoxfs thread did not send a result",
|
||||
)
|
||||
.into())
|
||||
}
|
||||
};
|
||||
|
||||
unmount_path(&mount_path)?;
|
||||
@@ -423,7 +465,11 @@ pub fn with_redoxfs<D, T, F>(disk: D, password_opt: Option<&[u8]>, callback: F)
|
||||
res
|
||||
}
|
||||
|
||||
pub fn fetch_bootloaders<S: AsRef<str>>(config: &Config, cookbook: Option<S>, live: bool) -> Result<(Vec<u8>, Vec<u8>)> {
|
||||
pub fn fetch_bootloaders<S: AsRef<str>>(
|
||||
config: &Config,
|
||||
cookbook: Option<S>,
|
||||
live: bool,
|
||||
) -> Result<(Vec<u8>, Vec<u8>)> {
|
||||
let bootloader_dir = format!("/tmp/redox_installer_bootloader_{}", process::id());
|
||||
|
||||
if Path::new(&bootloader_dir).exists() {
|
||||
@@ -434,7 +480,9 @@ pub fn fetch_bootloaders<S: AsRef<str>>(config: &Config, cookbook: Option<S>, li
|
||||
|
||||
let mut bootloader_config = Config::default();
|
||||
bootloader_config.general = config.general.clone();
|
||||
bootloader_config.packages.insert("bootloader".to_string(), PackageConfig::default());
|
||||
bootloader_config
|
||||
.packages
|
||||
.insert("bootloader".to_string(), PackageConfig::default());
|
||||
install_packages(&bootloader_config, &bootloader_dir, cookbook.as_ref());
|
||||
|
||||
let boot_dir = Path::new(&bootloader_dir).join("boot");
|
||||
@@ -466,10 +514,10 @@ pub fn fetch_bootloaders<S: AsRef<str>>(config: &Config, cookbook: Option<S>, li
|
||||
}
|
||||
|
||||
//TODO: make bootloaders use Option, dynamically create BIOS and EFI partitions
|
||||
pub fn with_whole_disk<P, F, T>(disk_path: P, disk_option: &DiskOption, callback: F)
|
||||
-> Result<T> where
|
||||
P: AsRef<Path>,
|
||||
F: FnOnce(&Path) -> Result<T>
|
||||
pub fn with_whole_disk<P, F, T>(disk_path: P, disk_option: &DiskOption, callback: F) -> Result<T>
|
||||
where
|
||||
P: AsRef<Path>,
|
||||
F: FnOnce(&Path) -> Result<T>,
|
||||
{
|
||||
let target = get_target();
|
||||
|
||||
@@ -495,7 +543,6 @@ pub fn with_whole_disk<P, F, T>(disk_path: P, disk_option: &DiskOption, callback
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// Calculate partition offsets
|
||||
let gpt_reserved = 34 * 512; // GPT always reserves 34 512-byte sectors
|
||||
let mibi = 1024 * 1024;
|
||||
@@ -506,7 +553,11 @@ pub fn with_whole_disk<P, F, T>(disk_path: P, disk_option: &DiskOption, callback
|
||||
|
||||
// Second megabyte of the disk is reserved for EFI partition
|
||||
let efi_start = bios_end + 1;
|
||||
let efi_size = if let Some(size) = disk_option.efi_partition_size { size as u64 } else { 1 };
|
||||
let efi_size = if let Some(size) = disk_option.efi_partition_size {
|
||||
size as u64
|
||||
} else {
|
||||
1
|
||||
};
|
||||
let efi_end = efi_start + (efi_size * mibi / block_size) - 1;
|
||||
|
||||
// The rest of the disk is RedoxFS, reserving the GPT table mirror at the end of disk
|
||||
@@ -516,7 +567,10 @@ pub fn with_whole_disk<P, F, T>(disk_path: P, disk_option: &DiskOption, callback
|
||||
// Format and install BIOS partition
|
||||
{
|
||||
// Write BIOS bootloader to disk
|
||||
eprintln!("Write bootloader with size {:#x}", disk_option.bootloader_bios.len());
|
||||
eprintln!(
|
||||
"Write bootloader with size {:#x}",
|
||||
disk_option.bootloader_bios.len()
|
||||
);
|
||||
disk_file.seek(SeekFrom::Start(0))?;
|
||||
disk_file.write_all(&disk_option.bootloader_bios)?;
|
||||
|
||||
@@ -536,37 +590,46 @@ pub fn with_whole_disk<P, F, T>(disk_path: P, disk_option: &DiskOption, callback
|
||||
// Add BIOS boot partition
|
||||
let mut partitions = BTreeMap::new();
|
||||
let mut partition_id = 1;
|
||||
partitions.insert(partition_id, gpt::partition::Partition {
|
||||
part_type_guid: gpt::partition_types::BIOS,
|
||||
part_guid: uuid::Uuid::new_v4(),
|
||||
first_lba: bios_start,
|
||||
last_lba: bios_end,
|
||||
flags: 0, // TODO
|
||||
name: "BIOS".to_string(),
|
||||
});
|
||||
partitions.insert(
|
||||
partition_id,
|
||||
gpt::partition::Partition {
|
||||
part_type_guid: gpt::partition_types::BIOS,
|
||||
part_guid: uuid::Uuid::new_v4(),
|
||||
first_lba: bios_start,
|
||||
last_lba: bios_end,
|
||||
flags: 0, // TODO
|
||||
name: "BIOS".to_string(),
|
||||
},
|
||||
);
|
||||
partition_id += 1;
|
||||
|
||||
// Add EFI boot partition
|
||||
partitions.insert(partition_id, gpt::partition::Partition {
|
||||
part_type_guid: gpt::partition_types::EFI,
|
||||
part_guid: uuid::Uuid::new_v4(),
|
||||
first_lba: efi_start,
|
||||
last_lba: efi_end,
|
||||
flags: 0, // TODO
|
||||
name: "EFI".to_string(),
|
||||
});
|
||||
partitions.insert(
|
||||
partition_id,
|
||||
gpt::partition::Partition {
|
||||
part_type_guid: gpt::partition_types::EFI,
|
||||
part_guid: uuid::Uuid::new_v4(),
|
||||
first_lba: efi_start,
|
||||
last_lba: efi_end,
|
||||
flags: 0, // TODO
|
||||
name: "EFI".to_string(),
|
||||
},
|
||||
);
|
||||
partition_id += 1;
|
||||
|
||||
// Add RedoxFS partition
|
||||
partitions.insert(partition_id, gpt::partition::Partition {
|
||||
//TODO: Use REDOX_REDOXFS type (needs GPT crate changes)
|
||||
part_type_guid: gpt::partition_types::LINUX_FS,
|
||||
part_guid: uuid::Uuid::new_v4(),
|
||||
first_lba: redoxfs_start,
|
||||
last_lba: redoxfs_end,
|
||||
flags: 0,
|
||||
name: "REDOX".to_string(),
|
||||
});
|
||||
partitions.insert(
|
||||
partition_id,
|
||||
gpt::partition::Partition {
|
||||
//TODO: Use REDOX_REDOXFS type (needs GPT crate changes)
|
||||
part_type_guid: gpt::partition_types::LINUX_FS,
|
||||
part_guid: uuid::Uuid::new_v4(),
|
||||
first_lba: redoxfs_start,
|
||||
last_lba: redoxfs_end,
|
||||
flags: 0,
|
||||
name: "REDOX".to_string(),
|
||||
},
|
||||
);
|
||||
|
||||
eprintln!("Writing GPT tables: {:#?}", partitions);
|
||||
|
||||
@@ -581,13 +644,13 @@ pub fn with_whole_disk<P, F, T>(disk_path: P, disk_option: &DiskOption, callback
|
||||
{
|
||||
let disk_efi_start = efi_start * block_size;
|
||||
let disk_efi_end = (efi_end + 1) * block_size;
|
||||
let mut disk_efi = fscommon::StreamSlice::new(
|
||||
&mut disk_file,
|
||||
disk_efi_start,
|
||||
disk_efi_end,
|
||||
)?;
|
||||
let mut disk_efi =
|
||||
fscommon::StreamSlice::new(&mut disk_file, disk_efi_start, disk_efi_end)?;
|
||||
|
||||
eprintln!("Formatting EFI partition with size {:#x}", disk_efi_end - disk_efi_start);
|
||||
eprintln!(
|
||||
"Formatting EFI partition with size {:#x}",
|
||||
disk_efi_end - disk_efi_start
|
||||
);
|
||||
fatfs::format_volume(&mut disk_efi, fatfs::FormatVolumeOptions::new())?;
|
||||
|
||||
eprintln!("Opening EFI partition");
|
||||
@@ -601,7 +664,11 @@ pub fn with_whole_disk<P, F, T>(disk_path: P, disk_option: &DiskOption, callback
|
||||
let efi_dir = root_dir.open_dir("EFI")?;
|
||||
efi_dir.create_dir("BOOT")?;
|
||||
|
||||
eprintln!("Writing EFI/BOOT/{} file with size {:#x}", bootloader_efi_name, disk_option.bootloader_efi.len());
|
||||
eprintln!(
|
||||
"Writing EFI/BOOT/{} file with size {:#x}",
|
||||
bootloader_efi_name,
|
||||
disk_option.bootloader_efi.len()
|
||||
);
|
||||
let boot_dir = efi_dir.open_dir("BOOT")?;
|
||||
let mut file = boot_dir.create_file(bootloader_efi_name)?;
|
||||
file.truncate()?;
|
||||
@@ -609,40 +676,38 @@ pub fn with_whole_disk<P, F, T>(disk_path: P, disk_option: &DiskOption, callback
|
||||
}
|
||||
|
||||
// Format and install RedoxFS partition
|
||||
eprintln!("Installing to RedoxFS partition with size {:#x}", (redoxfs_end - redoxfs_start) * block_size);
|
||||
eprintln!(
|
||||
"Installing to RedoxFS partition with size {:#x}",
|
||||
(redoxfs_end - redoxfs_start) * block_size
|
||||
);
|
||||
let disk_redoxfs = DiskIo(fscommon::StreamSlice::new(
|
||||
disk_file,
|
||||
redoxfs_start * block_size,
|
||||
(redoxfs_end + 1) * block_size
|
||||
(redoxfs_end + 1) * block_size,
|
||||
)?);
|
||||
with_redoxfs(
|
||||
disk_redoxfs,
|
||||
disk_option.password_opt,
|
||||
callback
|
||||
)
|
||||
with_redoxfs(disk_redoxfs, disk_option.password_opt, callback)
|
||||
}
|
||||
|
||||
pub fn install<P, S>(config: Config, output: P, cookbook: Option<S>, live: bool)
|
||||
-> Result<()> where
|
||||
P: AsRef<Path>,
|
||||
S: AsRef<str>,
|
||||
pub fn install<P, S>(config: Config, output: P, cookbook: Option<S>, live: bool) -> Result<()>
|
||||
where
|
||||
P: AsRef<Path>,
|
||||
S: AsRef<str>,
|
||||
{
|
||||
println!("Install {:#?} to {}", config, output.as_ref().display());
|
||||
|
||||
if output.as_ref().is_dir() {
|
||||
install_dir(config, output, cookbook)
|
||||
} else {
|
||||
let (bootloader_bios, bootloader_efi) = fetch_bootloaders(&config, cookbook.as_ref(), live)?;
|
||||
let (bootloader_bios, bootloader_efi) =
|
||||
fetch_bootloaders(&config, cookbook.as_ref(), live)?;
|
||||
let disk_option = DiskOption {
|
||||
bootloader_bios: &bootloader_bios,
|
||||
bootloader_efi: &bootloader_efi,
|
||||
password_opt: None,
|
||||
efi_partition_size: config.general.efi_partition_size,
|
||||
};
|
||||
with_whole_disk(output, &disk_option,
|
||||
move |mount_path| {
|
||||
install_dir(config, mount_path, cookbook)
|
||||
}
|
||||
)
|
||||
with_whole_disk(output, &disk_option, move |mount_path| {
|
||||
install_dir(config, mount_path, cookbook)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user