Skip to content

Commit

Permalink
feat: mise g bootstrap
Browse files Browse the repository at this point in the history
  • Loading branch information
jdx committed Dec 22, 2024
1 parent 40a13f8 commit fbb9c6d
Show file tree
Hide file tree
Showing 12 changed files with 223 additions and 6 deletions.
7 changes: 7 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ indoc = "2"
itertools = "0.13"
junction = "1"
log = "0.4"
minisign-verify = "0.2"
md-5 = "0.10"
num_cpus = "1"
number_prefix = "0.4"
Expand Down
9 changes: 9 additions & 0 deletions e2e/generate/test_generate_bootstrap
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
#!/usr/bin/env bash

# TODO: enable this after next release
#assert "mise generate bootstrap -w"
#assert "./bin/mise -v" "xx"
#
#assert "mise task add xxx -- echo 'running xxx'"
#assert "mise generate task-stubs --mise-bin ./bin/mise"
#assert "./bin/xxx" "running xxx"
5 changes: 5 additions & 0 deletions e2e/generate/test_generate_task_stubs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
#!/usr/bin/env bash

assert "mise task add xxx -- echo 'running xxx'"
assert "mise generate task-stubs"
assert "./bin/xxx" "running xxx"
4 changes: 2 additions & 2 deletions minisign.pub
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
untrusted comment: minisign public key E97C65762BC3EF8B
RWSL78MrdmV86brusnKIy6M7tCyiwUecPBqZ1w7U2vFPH40DuA2lKA1x
untrusted comment: minisign public key 64113EDF160FDEC2
RWTC3g8W3z4RZK3V3qv7fa1QY4JEWyBtqIHW+85QlJpZc5yG+uNYNBSZ
7 changes: 3 additions & 4 deletions packaging/standalone/install.envsubst
Original file line number Diff line number Diff line change
Expand Up @@ -193,21 +193,18 @@ after_finish_help() {
info "mise: run the following to activate mise in your shell:"
info "echo \"eval \\\"\\\$($install_path activate zsh)\\\"\" >> \"${ZDOTDIR-$HOME}/.zshrc\""
info ""
info "mise: this must be run in order to use mise in the terminal"
info "mise: run \`mise doctor\` to verify this is setup correctly"
;;
*/bash)
info "mise: run the following to activate mise in your shell:"
info "echo \"eval \\\"\\\$($install_path activate bash)\\\"\" >> ~/.bashrc"
info ""
info "mise: this must be run in order to use mise in the terminal"
info "mise: run \`mise doctor\` to verify this is setup correctly"
;;
*/fish)
info "mise: run the following to activate mise in your shell:"
info "echo \"$install_path activate fish | source\" >> ~/.config/fish/config.fish"
info ""
info "mise: this must be run in order to use mise in the terminal"
info "mise: run \`mise doctor\` to verify this is setup correctly"
;;
*)
Expand All @@ -217,4 +214,6 @@ after_finish_help() {
}

install_mise
after_finish_help
if [ "${MISE_INSTALL_HELP-}" != 0 ]; then
after_finish_help
fi
107 changes: 107 additions & 0 deletions src/cli/generate/bootstrap.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
use crate::config::SETTINGS;
use crate::{file, minisign};
use crate::http::HTTP;
use crate::ui::info;
use crate::Result;
use std::path::{PathBuf};
use clap::ValueHint;
use xx::file::display_path;
use xx::regex;

/// [experimental] Generate a script to download+execute mise
///
/// This is designed to be used in a project where contributors may not have mise installed.
#[derive(Debug, clap::Args)]
#[clap(verbatim_doc_comment, after_long_help = AFTER_LONG_HELP)]
pub struct Bootstrap {
/// Sandboxes mise internal directories like MISE_DATA_DIR and MISE_CACHE_DIR into a `.mise` directory in the project
///
/// This is necessary if users may use a different version of mise outside the project.
#[clap(long, short, verbatim_doc_comment)]
localize: bool,
/// Directory to put localized data into
#[clap(long, verbatim_doc_comment, default_value=".mise", value_hint=ValueHint::DirPath)]
localized_dir: PathBuf,
/// Specify mise version to fetch
#[clap(long, short='V', verbatim_doc_comment)]
version: Option<String>,
/// instead of outputting the script to stdout, write to a file and make it executable
#[clap(long, short, verbatim_doc_comment, num_args=0..=1, default_missing_value = "./bin/mise")]
write: Option<PathBuf>,
}

impl Bootstrap {
pub fn run(self) -> eyre::Result<()> {
SETTINGS.ensure_experimental("generate bootstrap")?;
let output = self.generate()?;
if let Some(bin) = &self.write {
if let Some(parent) = bin.parent() {
file::create_dir_all(parent)?;
}
file::write(bin, &output)?;
file::make_executable(bin)?;
miseprintln!("Wrote to {}", display_path(bin));
} else {
miseprintln!("{output}");
}
Ok(())
}

fn generate(&self) -> Result<String> {
let url = if let Some(v) = &self.version {
format!("https://mise.jdx.dev/v{v}/install.sh")
} else {
"https://mise.jdx.dev/install.sh".into()
};
let install = HTTP.get_text(&url)?;
// let install_sig = HTTP.get_text(format!("{url}.minisig"))?;
// minisign::verify(minisign::MISE_PUB_KEY, &install, &install_sig)?;
let install = info::indent_by(install, " ");
let version = regex!(r#"version="\$\{MISE_VERSION:-v([0-9.]+)\}""#).captures(&install).unwrap().get(1).unwrap().as_str();
let vars = if self.localize {
// TODO: this will only work right if it is in the base directory, not an absolute path or has a subdirectory
let localized_dir = self.localized_dir.to_string_lossy();
format!(r#"
local script_dir=$( cd -- "$( dirname -- "${{BASH_SOURCE[0]}}" )" &> /dev/null && pwd )
local project_dir=$( cd -- "$( dirname -- "$script_dir" )" &> /dev/null && pwd )
local localized_dir="$project_dir/{localized_dir}"
export MISE_DATA_DIR="$localized_dir"
export MISE_CONFIG_DIR="$localized_dir"
export MISE_CACHE_DIR="$localized_dir/cache"
export MISE_STATE_DIR="$localized_dir/state"
export MISE_INSTALL_PATH="$localized_dir/mise-{version}"
"#)
} else {
format!(r#"
local cache_home="${{XDG_CACHE_HOME:-$HOME/.cache}}/mise"
export MISE_INSTALL_PATH="$cache_home/mise-{version}"
"#)
};
let vars = info::indent_by(vars.trim(), " ");
let script = format!(
r#"
#!/bin/sh
set -euxo pipefail
__mise_bootstrap() {{
{vars}
install() {{
{install}
}}
test -f "$MISE_INSTALL_PATH" || install
}}
__mise_bootstrap
exec "$MISE_INSTALL_PATH" "$@"
"#);
Ok(script.trim().to_string())
}
}

static AFTER_LONG_HELP: &str = color_print::cstr!(
r#"<bold><underline>Examples:</underline></bold>
$ <bold>mise generate bootstrap >./bin/mise</bold>
$ <bold>chmod +x ./bin/mise</bold>
$ <bold>./bin/mise install</bold> – automatically downloads mise to .mise if not already installed
"#
);
6 changes: 6 additions & 0 deletions src/cli/generate/mod.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
use clap::Subcommand;

mod bootstrap;
mod config;
mod git_pre_commit;
mod github_action;
mod task_docs;
mod task_stubs;

/// [experimental] Generate files for various tools/services
#[derive(Debug, clap::Args)]
Expand All @@ -15,19 +17,23 @@ pub struct Generate {

#[derive(Debug, Subcommand)]
enum Commands {
Bootstrap(bootstrap::Bootstrap),
Config(config::Config),
GitPreCommit(git_pre_commit::GitPreCommit),
GithubAction(github_action::GithubAction),
TaskDocs(task_docs::TaskDocs),
TaskStubs(task_stubs::TaskStubs),
}

impl Commands {
pub fn run(self) -> eyre::Result<()> {
match self {
Self::Bootstrap(cmd) => cmd.run(),
Self::Config(cmd) => cmd.run(),
Self::GitPreCommit(cmd) => cmd.run(),
Self::GithubAction(cmd) => cmd.run(),
Self::TaskDocs(cmd) => cmd.run(),
Self::TaskStubs(cmd) => cmd.run(),
}
}
}
Expand Down
68 changes: 68 additions & 0 deletions src/cli/generate/task_stubs.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
use crate::config::{Config, SETTINGS};
use crate::{file, minisign};
use crate::http::HTTP;
use crate::ui::info;
use crate::Result;
use std::path::{PathBuf};
use clap::ValueHint;
use xx::file::display_path;
use xx::regex;
use crate::task::Task;

/// [experimental] Generates shims to run mise tasks
///
/// By default, this will build shims like ./bin/<task>. These can be paired with `mise generate bootstrap`
/// so contributors to a project can execute mise tasks without installing mise into their system.
#[derive(Debug, clap::Args)]
#[clap(verbatim_doc_comment, after_long_help = AFTER_LONG_HELP)]
pub struct TaskStubs {
/// Path to a mise bin to use when running the task stub.
///
/// Use `--mise-bin=./bin/mise` to use a mise bin generated from `mise generate bootstrap`
#[clap(long, short, verbatim_doc_comment, default_value="mise")]
mise_bin: PathBuf,

/// Directory to create task stubs inside of
#[clap(long, short, verbatim_doc_comment, default_value="bin", value_hint=ValueHint::DirPath)]
dir: PathBuf,
}

impl TaskStubs {
pub fn run(self) -> Result<()> {
SETTINGS.ensure_experimental("generate task-stubs")?;
let config = Config::get();
for task in config.tasks()?.values() {
let bin = self.dir.join(task.name_to_path());
let output = self.generate(task)?;
if let Some(parent) = bin.parent() {
file::create_dir_all(parent)?;
}
file::write(&bin, &output)?;
file::make_executable(&bin)?;
miseprintln!("Wrote to {}", display_path(&bin));
}
Ok(())
}

fn generate(&self, task: &Task) -> Result<String> {
let mise_bin = self.mise_bin.to_string_lossy();
let mise_bin = shell_words::quote(&mise_bin);
let display_name = task.display_name();
let script = format!(
r#"
#!/bin/sh
exec {mise_bin} run {display_name} "$@"
"#);
Ok(script.trim().to_string())
}
}

static AFTER_LONG_HELP: &str = color_print::cstr!(
r#"<bold><underline>Examples:</underline></bold>
$ <bold>mise task add test -- echo 'running tests'</bold>
$ <bold>mise generate task-stubs</bold>
$ <bold>./bin/test</bold>
running tests
"#
);
1 change: 1 addition & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ mod uv;
mod versions_host;
mod watch_files;
mod wildcard;
mod minisign;

pub(crate) use crate::exit::exit;
pub(crate) use crate::result::Result;
Expand Down
10 changes: 10 additions & 0 deletions src/minisign.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
use crate::*;
use minisign_verify::*;
pub const MISE_PUB_KEY: &str = include_str!("../minisign.pub");

pub fn verify(pub_key: &str, data: &str, sig: &str) -> Result<()> {
let public_key = PublicKey::from_base64(pub_key)?;
let signature = Signature::decode(sig)?;
public_key.verify(data.as_bytes(), &signature, false)?;
Ok(())
}
4 changes: 4 additions & 0 deletions src/task/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -475,6 +475,10 @@ impl Task {
}
Ok(())
}

pub fn name_to_path(&self) -> PathBuf {
self.name.replace(':', path::MAIN_SEPARATOR_STR).into()
}

pub fn render_env(&self, ts: &Toolset) -> Result<EnvMap> {
let config = Config::get();
Expand Down

0 comments on commit fbb9c6d

Please sign in to comment.