use std::sync::Arc;
use eyre::Result;
use serde_json::json;
use super::install::Install;
use super::run;
use super::system::driver::{self, Action, DriverOpts};
use super::system::{install, status, upgrade, r#use};
use crate::config::{self, Config, Settings};
use crate::dirs;
use crate::system;
use crate::system::defaults::DefaultsState;
use crate::system::files::{FileMode, FileRequest};
use crate::system::hooks::{self, BootstrapHookPhase};
use crate::system::launchd::LaunchdState;
use crate::system::login_shell::LoginShellState;
use crate::system::systemd::SystemdState;
use crate::ui::table::MiseTable;
use clap::Subcommand;
#[derive(Debug, clap::Args)]
#[clap(verbatim_doc_comment, after_long_help = AFTER_LONG_HELP)]
pub struct Bootstrap {
#[clap(subcommand)]
command: Option<Commands>,
#[clap(long, short = 'n')]
dry_run: bool,
#[clap(long, short)]
yes: bool,
#[clap(long)]
force_dotfiles: bool,
#[clap(long)]
update: bool,
}
#[derive(Debug, Subcommand)]
enum Commands {
Launchd(BootstrapLaunchd),
MacosDefaults(BootstrapMacosDefaults),
Packages(BootstrapPackages),
Systemd(BootstrapSystemd),
User(BootstrapUser),
}
#[derive(Debug, clap::Args)]
#[clap(verbatim_doc_comment)]
struct BootstrapPackages {
#[clap(subcommand)]
command: BootstrapPackagesCommands,
}
#[derive(Debug, Subcommand)]
enum BootstrapPackagesCommands {
#[cfg(unix)]
Brew(super::system::brew::SystemBrew),
Install(install::SystemInstall),
Status(status::SystemStatus),
Upgrade(upgrade::SystemUpgrade),
Use(r#use::SystemUse),
}
#[derive(Debug, clap::Args)]
#[clap(verbatim_doc_comment)]
struct BootstrapMacosDefaults {
#[clap(subcommand)]
command: BootstrapMacosDefaultsCommands,
}
#[derive(Debug, Subcommand)]
enum BootstrapMacosDefaultsCommands {
Apply(BootstrapMacosDefaultsApply),
Status(BootstrapMacosDefaultsStatus),
}
#[derive(Debug, clap::Args)]
struct BootstrapMacosDefaultsApply {
#[clap(long, short = 'n')]
dry_run: bool,
#[clap(long, short)]
yes: bool,
}
#[derive(Debug, clap::Args)]
struct BootstrapMacosDefaultsStatus {
#[clap(long, short = 'J')]
json: bool,
#[clap(long, verbatim_doc_comment)]
missing: bool,
}
#[derive(Debug, clap::Args)]
#[clap(verbatim_doc_comment)]
struct BootstrapLaunchd {
#[clap(subcommand)]
command: BootstrapLaunchdCommands,
}
#[derive(Debug, Subcommand)]
enum BootstrapLaunchdCommands {
Apply(BootstrapLaunchdApply),
Status(BootstrapLaunchdStatus),
}
#[derive(Debug, clap::Args)]
struct BootstrapLaunchdApply {
#[clap(long, short = 'n')]
dry_run: bool,
#[clap(long, short)]
yes: bool,
}
#[derive(Debug, clap::Args)]
struct BootstrapLaunchdStatus {
#[clap(long, short = 'J')]
json: bool,
#[clap(long, verbatim_doc_comment)]
missing: bool,
}
#[derive(Debug, clap::Args)]
#[clap(verbatim_doc_comment)]
struct BootstrapSystemd {
#[clap(subcommand)]
command: BootstrapSystemdCommands,
}
#[derive(Debug, Subcommand)]
enum BootstrapSystemdCommands {
Apply(BootstrapSystemdApply),
Status(BootstrapSystemdStatus),
}
#[derive(Debug, clap::Args)]
struct BootstrapSystemdApply {
#[clap(long, short = 'n')]
dry_run: bool,
#[clap(long, short)]
yes: bool,
}
#[derive(Debug, clap::Args)]
struct BootstrapSystemdStatus {
#[clap(long, short = 'J')]
json: bool,
#[clap(long, verbatim_doc_comment)]
missing: bool,
}
#[derive(Debug, clap::Args)]
#[clap(verbatim_doc_comment)]
struct BootstrapUser {
#[clap(subcommand)]
command: BootstrapUserCommands,
}
#[derive(Debug, Subcommand)]
enum BootstrapUserCommands {
Apply(BootstrapUserApply),
Status(BootstrapUserStatus),
}
#[derive(Debug, clap::Args)]
struct BootstrapUserApply {
#[clap(long, short = 'n')]
dry_run: bool,
#[clap(long, short)]
yes: bool,
}
#[derive(Debug, clap::Args)]
struct BootstrapUserStatus {
#[clap(long, short = 'J')]
json: bool,
#[clap(long, verbatim_doc_comment)]
missing: bool,
}
impl Bootstrap {
pub async fn run(self) -> Result<()> {
Settings::get().ensure_experimental("mise bootstrap")?;
if let Some(command) = self.command {
return command.run().await;
}
let mut config = Config::get().await?;
let mut hooks = system::hooks_from_config(&config);
self.run_hooks(&hooks, BootstrapHookPhase::PrePackages)
.await?;
let mgrs = system::packages_from_config(&config);
if mgrs.is_empty() {
debug!("bootstrap: no [bootstrap.packages] configured, skipping");
} else {
info!("bootstrap: system packages");
let opts = DriverOpts {
manager: None,
explicit: false,
dry_run: self.dry_run,
update: self.update,
yes: self.yes,
};
driver::run(mgrs, Action::Install, &opts).await?;
}
self.run_hooks(&hooks, BootstrapHookPhase::PostPackages)
.await?;
self.run_hooks(&hooks, BootstrapHookPhase::PreDotfiles)
.await?;
let files = system::files::files_from_config(&config);
if files.is_empty() {
debug!("bootstrap: no whole-file [dotfiles] entries configured, skipping");
} else {
info!("bootstrap: dotfiles");
let opts = system::files::ApplyOpts {
dry_run: self.dry_run,
verbose: false,
force: self.force_dotfiles,
force_hint: "use --force-dotfiles or run `mise dotfiles apply --force`",
yes: self.yes,
};
system::files::apply(&config, &files, &opts)?;
}
let edits = system::edits::edits_from_config(&config);
if edits.is_empty() {
debug!("bootstrap: no edit [dotfiles] entries configured, skipping");
} else {
info!("bootstrap: dotfile edits");
let opts = system::edits::ApplyOpts {
dry_run: self.dry_run,
verbose: false,
yes: self.yes,
};
system::edits::apply(&config, &edits, &opts)?;
}
if self.dry_run {
hooks = self.hooks_after_dotfiles_dry_run(&config, &files)?;
} else {
config = Config::reset().await?;
hooks = system::hooks_from_config(&config);
}
self.run_hooks(&hooks, BootstrapHookPhase::PostDotfiles)
.await?;
self.run_hooks(&hooks, BootstrapHookPhase::PreDefaults)
.await?;
let defaults = system::defaults_from_config(&config);
if defaults.is_empty() {
debug!("bootstrap: no [bootstrap.macos.defaults] configured, skipping");
} else {
info!("bootstrap: system defaults");
install::apply_defaults(defaults, self.dry_run, self.yes).await?;
}
self.run_hooks(&hooks, BootstrapHookPhase::PostDefaults)
.await?;
let agents = system::launchd_from_config(&config);
if agents.is_empty() {
debug!("bootstrap: no [bootstrap.macos.launchd.agents] configured, skipping");
} else {
info!("bootstrap: launchd agents");
install::apply_launchd(agents, self.dry_run, self.yes).await?;
}
let units = system::systemd_from_config(&config);
if units.is_empty() {
debug!("bootstrap: no [bootstrap.linux.systemd.units] configured, skipping");
} else {
info!("bootstrap: systemd user services");
install::apply_systemd(units, self.dry_run, self.yes).await?;
}
self.run_hooks(&hooks, BootstrapHookPhase::PreUser).await?;
let login_shell = system::login_shell_from_config(&config);
if login_shell.is_none() {
debug!("bootstrap: no [bootstrap.user].login_shell configured, skipping");
} else {
info!("bootstrap: login shell");
install::apply_login_shell(login_shell, self.dry_run, self.yes)?;
}
self.run_hooks(&hooks, BootstrapHookPhase::PostUser).await?;
self.run_hooks(&hooks, BootstrapHookPhase::PreTools).await?;
info!("bootstrap: tools");
Install::new_bare(self.dry_run).run().await?;
if !self.dry_run {
config = Config::reset().await?;
hooks = system::hooks_from_config(&config);
}
self.run_hooks(&hooks, BootstrapHookPhase::PostTools)
.await?;
let tasks = config.tasks().await?;
if tasks.iter().any(|(_, t)| t.is_match("bootstrap")) {
info!("bootstrap: running `bootstrap` task");
self.run_task("bootstrap").await?;
} else {
debug!("bootstrap: no `bootstrap` task defined, skipping");
}
self.run_hooks(&hooks, BootstrapHookPhase::Final).await?;
Ok(())
}
async fn run_hooks(
&self,
hooks: &[hooks::BootstrapHook],
phase: BootstrapHookPhase,
) -> Result<()> {
hooks::run_phase(hooks, phase, self.dry_run).await
}
fn hooks_after_dotfiles_dry_run(
&self,
config: &Config,
files: &[FileRequest],
) -> Result<Vec<hooks::BootstrapHook>> {
let mut config_files = config.config_files.clone();
for file in files {
if !is_mise_config_target(&file.target) || !file.source.is_file() {
continue;
}
match parse_dotfile_mise_config(config, file) {
Ok(cf) => {
config_files.insert(file.target.clone(), cf);
}
Err(err) => {
warn!(
"[dotfiles].\"{}\": failed to parse config source {}: {err}",
file.target_raw,
file.source.display()
);
}
}
}
Ok(system::hooks_from_config_files(&config_files))
}
async fn run_task(&self, task: &str) -> Result<()> {
run::Run {
task: task.into(),
args: vec![],
args_last: vec![],
cd: None,
continue_on_error: false,
dry_run: self.dry_run,
force: false,
is_linear: false,
jobs: None,
no_timings: false,
output: None,
shell: None,
quiet: false,
silent: false,
raw: false,
timings: false,
tmpdir: Default::default(),
tool: Default::default(),
output_handler: None,
context_builder: Default::default(),
executor: None,
no_cache: Default::default(),
timeout: None,
skip_deps: false,
skip_tools: self.dry_run,
no_deps: false,
fresh_env: false,
deny_all: false,
deny_read: false,
deny_write: false,
deny_net: false,
deny_env: false,
allow_read: vec![],
allow_write: vec![],
allow_net: vec![],
allow_env: vec![],
}
.run()
.await
}
}
fn parse_dotfile_mise_config(
config: &Config,
file: &FileRequest,
) -> Result<Arc<dyn config::config_file::ConfigFile>> {
let body = match file.mode {
FileMode::Template => system::files::render_template(config, file)?,
_ => crate::file::read_to_string(&file.source)?,
};
Ok(Arc::new(
config::config_file::mise_toml::MiseToml::from_str(&body, &file.target)?,
))
}
fn is_mise_config_target(path: &std::path::Path) -> bool {
path.starts_with(*dirs::CONFIG)
|| path.starts_with(*dirs::SYSTEM_CONFIG)
|| config::DEFAULT_CONFIG_FILENAMES.iter().any(|filename| {
filename.ends_with(".toml") && !filename.contains('*') && path.ends_with(filename)
})
|| (path.extension().is_some_and(|ext| ext == "toml")
&& path
.parent()
.is_some_and(|parent| parent.ends_with(".config/mise/conf.d")))
}
impl Commands {
async fn run(self) -> Result<()> {
match self {
Self::Launchd(cmd) => cmd.run().await,
Self::MacosDefaults(cmd) => cmd.run().await,
Self::Packages(cmd) => cmd.run().await,
Self::Systemd(cmd) => cmd.run().await,
Self::User(cmd) => cmd.run().await,
}
}
}
impl BootstrapPackages {
async fn run(self) -> Result<()> {
Settings::get().ensure_experimental("mise bootstrap")?;
match self.command {
#[cfg(unix)]
BootstrapPackagesCommands::Brew(cmd) => cmd.run().await,
BootstrapPackagesCommands::Install(cmd) => cmd.run().await,
BootstrapPackagesCommands::Status(cmd) => cmd.run().await,
BootstrapPackagesCommands::Upgrade(cmd) => cmd.run().await,
BootstrapPackagesCommands::Use(cmd) => cmd.run().await,
}
}
}
impl BootstrapMacosDefaults {
async fn run(self) -> Result<()> {
Settings::get().ensure_experimental("mise bootstrap")?;
match self.command {
BootstrapMacosDefaultsCommands::Apply(cmd) => cmd.run().await,
BootstrapMacosDefaultsCommands::Status(cmd) => cmd.run().await,
}
}
}
impl BootstrapLaunchd {
async fn run(self) -> Result<()> {
Settings::get().ensure_experimental("mise bootstrap")?;
match self.command {
BootstrapLaunchdCommands::Apply(cmd) => cmd.run().await,
BootstrapLaunchdCommands::Status(cmd) => cmd.run().await,
}
}
}
impl BootstrapLaunchdApply {
async fn run(self) -> Result<()> {
let config = Config::get().await?;
install::apply_launchd(system::launchd_from_config(&config), self.dry_run, self.yes).await
}
}
impl BootstrapLaunchdStatus {
async fn run(self) -> Result<()> {
let config = Config::get().await?;
let agents = system::launchd_from_config(&config);
let mut any_missing = false;
let mut rows: Vec<Vec<String>> = vec![];
let mut json_out = serde_json::Map::new();
if !agents.is_empty() {
if !system::launchd::is_available() {
let reason = system::launchd::unavailable_reason();
if self.json {
json_out.insert(
"launchd".to_string(),
json!({ "available": false, "reason": reason }),
);
} else {
for req in &agents {
rows.push(vec![
req.name.clone(),
req.label.clone(),
"".to_string(),
format!("skipped ({reason})"),
]);
}
}
} else {
let statuses = system::launchd::status(&agents).await?;
let mut json_entries = vec![];
for s in statuses {
let state = match &s.state {
LaunchdState::Loaded => "loaded",
LaunchdState::Unloaded => {
any_missing = true;
"unloaded"
}
LaunchdState::Differs => {
any_missing = true;
"differs"
}
LaunchdState::Missing => {
any_missing = true;
"missing"
}
};
if self.json {
json_entries.push(json!({
"name": s.request.name,
"label": s.request.label,
"path": s.path,
"loaded": s.loaded,
"state": state,
}));
} else {
rows.push(vec![
s.request.name.clone(),
s.request.label.clone(),
s.path.display().to_string(),
state.to_string(),
]);
}
}
if self.json {
json_out.insert(
"launchd".to_string(),
json!({ "available": true, "agents": json_entries }),
);
}
}
}
if self.json {
miseprintln!("{}", serde_json::to_string_pretty(&json_out)?);
} else if rows.is_empty() {
info!("nothing configured in [bootstrap.macos.launchd.agents]");
} else {
let mut table = MiseTable::new(false, &["Name", "Label", "Path", "State"]);
for row in rows {
table.add_row(row);
}
table.print()?;
}
if self.missing && any_missing {
crate::exit(1);
}
Ok(())
}
}
impl BootstrapSystemd {
async fn run(self) -> Result<()> {
Settings::get().ensure_experimental("mise bootstrap")?;
match self.command {
BootstrapSystemdCommands::Apply(cmd) => cmd.run().await,
BootstrapSystemdCommands::Status(cmd) => cmd.run().await,
}
}
}
impl BootstrapSystemdApply {
async fn run(self) -> Result<()> {
let config = Config::get().await?;
install::apply_systemd(system::systemd_from_config(&config), self.dry_run, self.yes).await
}
}
impl BootstrapSystemdStatus {
async fn run(self) -> Result<()> {
let config = Config::get().await?;
let units = system::systemd_from_config(&config);
let mut any_missing = false;
let mut rows: Vec<Vec<String>> = vec![];
let mut json_out = serde_json::Map::new();
if !units.is_empty() {
if !system::systemd::is_available() {
let reason = system::systemd::unavailable_reason();
if self.json {
json_out.insert(
"systemd".to_string(),
json!({ "available": false, "reason": reason }),
);
} else {
for req in &units {
rows.push(vec![
req.name.clone(),
req.unit.clone(),
"".to_string(),
format!("skipped ({reason})"),
]);
}
}
} else {
let statuses = system::systemd::status(&units).await?;
let mut json_entries = vec![];
for s in statuses {
let desired = s.is_desired();
let state = match &s.state {
SystemdState::Active => "active",
SystemdState::Inactive => "inactive",
SystemdState::Differs => {
any_missing = true;
"differs"
}
SystemdState::Missing => {
any_missing = true;
"missing"
}
};
if !desired {
any_missing = true;
}
if self.json {
json_entries.push(json!({
"name": s.request.name,
"unit": s.request.unit,
"path": s.path,
"active": s.active,
"enabled": s.enabled,
"desired": desired,
"state": state,
}));
} else {
rows.push(vec![
s.request.name.clone(),
s.request.unit.clone(),
s.path.display().to_string(),
state.to_string(),
]);
}
}
if self.json {
json_out.insert(
"systemd".to_string(),
json!({ "available": true, "units": json_entries }),
);
}
}
}
if self.json {
miseprintln!("{}", serde_json::to_string_pretty(&json_out)?);
} else if rows.is_empty() {
info!("nothing configured in [bootstrap.linux.systemd.units]");
} else {
let mut table = MiseTable::new(false, &["Name", "Unit", "Path", "State"]);
for row in rows {
table.add_row(row);
}
table.print()?;
}
if self.missing && any_missing {
crate::exit(1);
}
Ok(())
}
}
impl BootstrapMacosDefaultsApply {
async fn run(self) -> Result<()> {
let config = Config::get().await?;
install::apply_defaults(
system::defaults_from_config(&config),
self.dry_run,
self.yes,
)
.await
}
}
impl BootstrapMacosDefaultsStatus {
async fn run(self) -> Result<()> {
let config = Config::get().await?;
let defaults = system::defaults_from_config(&config);
let mut any_missing = false;
let mut rows: Vec<Vec<String>> = vec![];
let mut json_out = serde_json::Map::new();
if !defaults.is_empty() {
if !system::defaults::is_available() {
let reason = system::defaults::unavailable_reason();
if self.json {
json_out.insert(
"macos_defaults".to_string(),
json!({ "available": false, "reason": reason }),
);
} else {
for req in &defaults {
rows.push(vec![
req.domain.clone(),
req.key.clone(),
req.value.to_string(),
"".to_string(),
format!("skipped ({reason})"),
]);
}
}
} else {
let statuses = system::defaults::status(&defaults).await?;
let mut json_entries = vec![];
for s in statuses {
let (current, state) = match &s.state {
DefaultsState::Set => (s.request.value.to_string(), "set"),
DefaultsState::Differs { current } => {
any_missing = true;
(current.clone(), "differs")
}
DefaultsState::Unset => {
any_missing = true;
("".to_string(), "unset")
}
};
if self.json {
json_entries.push(json!({
"domain": s.request.domain,
"key": s.request.key,
"value": s.request.value.to_json(),
"current": current,
"state": state,
}));
} else {
rows.push(vec![
s.request.domain.clone(),
s.request.key.clone(),
s.request.value.to_string(),
current,
state.to_string(),
]);
}
}
if self.json {
json_out.insert(
"macos_defaults".to_string(),
json!({ "available": true, "entries": json_entries }),
);
}
}
}
if self.json {
miseprintln!("{}", serde_json::to_string_pretty(&json_out)?);
} else if rows.is_empty() {
info!("nothing configured in [bootstrap.macos.defaults]");
} else {
let mut table = MiseTable::new(false, &["Domain", "Key", "Value", "Current", "State"]);
for row in rows {
table.add_row(row);
}
table.print()?;
}
if self.missing && any_missing {
crate::exit(1);
}
Ok(())
}
}
impl BootstrapUser {
async fn run(self) -> Result<()> {
Settings::get().ensure_experimental("mise bootstrap")?;
match self.command {
BootstrapUserCommands::Apply(cmd) => cmd.run().await,
BootstrapUserCommands::Status(cmd) => cmd.run().await,
}
}
}
impl BootstrapUserApply {
async fn run(self) -> Result<()> {
let config = Config::get().await?;
install::apply_login_shell(
system::login_shell_from_config(&config),
self.dry_run,
self.yes,
)
}
}
impl BootstrapUserStatus {
async fn run(self) -> Result<()> {
let config = Config::get().await?;
let login_shell = system::login_shell_from_config(&config);
let mut any_missing = false;
let mut rows: Vec<Vec<String>> = vec![];
let mut json_out = serde_json::Map::new();
if let Some(req) = login_shell {
if !system::login_shell::is_available() {
let reason = system::login_shell::unavailable_reason();
if self.json {
json_out.insert(
"login_shell".to_string(),
json!({
"available": false,
"reason": reason,
"shell": req.shell,
}),
);
} else {
rows.push(vec![
req.shell,
"".to_string(),
format!("skipped ({reason})"),
]);
}
} else {
let status = system::login_shell::status(&req)?;
let state = match &status.state {
LoginShellState::Set => "set",
LoginShellState::Differs { .. } => {
any_missing = true;
"differs"
}
LoginShellState::MissingFromShells { .. } => {
any_missing = true;
"missing from /etc/shells"
}
};
if self.json {
json_out.insert(
"login_shell".to_string(),
json!({
"available": true,
"shell": status.request.shell,
"user": status.user,
"current": status.current,
"shell_listed": status.shell_listed,
"state": state,
}),
);
} else {
rows.push(vec![
status.request.shell,
status.current,
state.to_string(),
]);
}
}
}
if self.json {
miseprintln!("{}", serde_json::to_string_pretty(&json_out)?);
} else if rows.is_empty() {
info!("nothing configured in [bootstrap.user]");
} else {
let mut table = MiseTable::new(false, &["Shell", "Current", "State"]);
for row in rows {
table.add_row(row);
}
table.print()?;
}
if self.missing && any_missing {
crate::exit(1);
}
Ok(())
}
}
static AFTER_LONG_HELP: &str = color_print::cstr!(
r#"<bold><underline>Examples:</underline></bold>
$ <bold>mise bootstrap</bold> # packages + dotfiles + tools + bootstrap task
$ <bold>mise bootstrap --force-dotfiles</bold> # replace conflicting dotfile targets
$ <bold>mise bootstrap packages install --yes</bold>
$ <bold>mise bootstrap macos-defaults status</bold>
$ <bold>mise bootstrap launchd apply --dry-run</bold>
$ <bold>mise bootstrap systemd apply --dry-run</bold>
$ <bold>mise bootstrap user apply --dry-run</bold>
"#
);