├── .dockerignore ├── rust-toolchain.toml ├── .gitignore ├── src ├── cmd │ ├── list.rs │ ├── switch.rs │ ├── releases.rs │ ├── default.rs │ ├── update_links.rs │ ├── component_install.rs │ ├── install.rs │ └── build.rs ├── cmd.rs ├── git.rs ├── components │ ├── rebar3.rs │ └── elp.rs ├── links.rs ├── languages │ ├── elixir.rs │ ├── gleam.rs │ └── erlang.rs ├── utils.rs ├── components.rs ├── run.rs ├── languages.rs ├── github.rs ├── main.rs └── config.rs ├── shelltests ├── macos │ └── install_erlang.test ├── languages_components.test └── installs.test ├── Dockerfile ├── oranda.json ├── .github └── workflows │ ├── windows-compile.yml │ ├── shelltests.yml │ ├── web.yml │ └── release.yml ├── Justfile ├── Cargo.toml ├── cliff.toml ├── CHANGELOG.md ├── README.md ├── LICENSE └── wix └── main.wxs /.dockerignore: -------------------------------------------------------------------------------- 1 | target 2 | .git -------------------------------------------------------------------------------- /rust-toolchain.toml: -------------------------------------------------------------------------------- 1 | [toolchain] 2 | channel = "1.91.1" 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | .beamup.toml 3 | 4 | # Generated by `oranda generate ci` 5 | public/ -------------------------------------------------------------------------------- /src/cmd/list.rs: -------------------------------------------------------------------------------- 1 | use crate::config; 2 | 3 | pub fn run(config: &config::Config) { 4 | config::print_ids(config); 5 | } 6 | -------------------------------------------------------------------------------- /src/cmd.rs: -------------------------------------------------------------------------------- 1 | pub mod build; 2 | pub mod component_install; 3 | pub mod default; 4 | pub mod install; 5 | pub mod list; 6 | pub mod releases; 7 | pub mod switch; 8 | pub mod update_links; 9 | -------------------------------------------------------------------------------- /shelltests/macos/install_erlang.test: -------------------------------------------------------------------------------- 1 | # test an install of erlang on macos 2 | target/debug/beamup install erlang OTP-27.1 3 | >>>=0 4 | 5 | find /Users/runner/Library -name erlc 6 | >>>=0 7 | 8 | DEBUG=1 ~/.beamup/bin/erlc 9 | >>>=0 -------------------------------------------------------------------------------- /src/cmd/switch.rs: -------------------------------------------------------------------------------- 1 | use crate::config; 2 | use crate::languages::Language; 3 | use color_eyre::eyre::Result; 4 | 5 | pub fn run(language: &Language, id: &str, config: config::Config) -> Result<()> { 6 | config::switch(language, id, &config) 7 | } 8 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM opensuse/leap:15.6 2 | 3 | WORKDIR /app 4 | 5 | RUN zypper --non-interactive install shelltestrunner rustup && rustup toolchain install stable 6 | 7 | RUN zypper --non-interactive install just 8 | 9 | ENTRYPOINT ["just"] 10 | -------------------------------------------------------------------------------- /src/cmd/releases.rs: -------------------------------------------------------------------------------- 1 | use crate::github::print_releases; 2 | use crate::languages; 3 | 4 | pub fn run(installable: &T) { 5 | // TODO: source repo and binary repo could have different releases to print 6 | print_releases(&installable.source_repo()); 7 | } 8 | -------------------------------------------------------------------------------- /oranda.json: -------------------------------------------------------------------------------- 1 | { 2 | "styles": { 3 | "theme": "axolight" 4 | }, 5 | "build": { 6 | "path_prefix": "beamup" 7 | }, 8 | "marketing": { 9 | "social": { 10 | "image": "https://i.imgur.com/fFx86RE.png", 11 | "image_alt": "beamup logo" 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /shelltests/languages_components.test: -------------------------------------------------------------------------------- 1 | # test languages works 2 | target/debug/beamup languages 3 | >>> 4 | Languages: 5 | 6 | Elixir 7 | Erlang 8 | Gleam 9 | >>>= 0 10 | 11 | # test components works 12 | target/debug/beamup components 13 | >>> 14 | Components: 15 | 16 | Elp 17 | Rebar3 18 | >>>= 0 19 | -------------------------------------------------------------------------------- /src/cmd/default.rs: -------------------------------------------------------------------------------- 1 | use crate::config; 2 | use crate::languages::Language; 3 | use color_eyre::eyre::Result; 4 | 5 | pub fn run( 6 | language: &Language, 7 | id: &String, 8 | config_file: String, 9 | config: config::Config, 10 | ) -> Result<()> { 11 | config::set_default(language, id, config_file, config) 12 | } 13 | -------------------------------------------------------------------------------- /.github/workflows/windows-compile.yml: -------------------------------------------------------------------------------- 1 | name: Shelltests 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | jobs: 10 | run: 11 | strategy: 12 | matrix: 13 | os: ["windows-latest"] 14 | runs-on: ${{ matrix.os }} 15 | steps: 16 | - name: Checkout 17 | uses: actions/checkout@v4 18 | 19 | - name: Build beamup 20 | run: cargo build 21 | -------------------------------------------------------------------------------- /src/git.rs: -------------------------------------------------------------------------------- 1 | use crate::github::GithubRelease; 2 | // use http::Uri; 3 | 4 | pub enum GitRef { 5 | Branch(String), 6 | Release(GithubRelease), 7 | } 8 | 9 | impl std::fmt::Display for GitRef { 10 | fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { 11 | match self { 12 | GitRef::Branch(b) => write!(f, "{}", b), 13 | GitRef::Release(r) => write!(f, "{}", r), 14 | } 15 | } 16 | } 17 | 18 | // pub struct GitInfo { 19 | // repo: Uri, 20 | // } 21 | -------------------------------------------------------------------------------- /Justfile: -------------------------------------------------------------------------------- 1 | build: 2 | cargo build 3 | 4 | docker-image: 5 | docker build -t beamup-shelltest . 6 | 7 | docker-run-shelltests: docker-image 8 | docker run -v $(pwd):/app beamup-shelltest in-docker-shelltests 9 | 10 | # have to copy to a new dir outside of the mounted volume or 11 | # we get an error when the link is created 12 | in-docker-shelltests: build 13 | mkdir /tmp/app 14 | cp -R . /tmp/app 15 | cd /tmp/app && just shelltests 16 | 17 | shelltests: 18 | shelltest -c --diff --debug --all shelltests/*.test 19 | 20 | shelltests-macos: 21 | shelltest -c --diff --debug --all shelltests/macos/*.test 22 | -------------------------------------------------------------------------------- /src/cmd/update_links.rs: -------------------------------------------------------------------------------- 1 | use crate::config; 2 | use crate::languages; 3 | use crate::links; 4 | use crate::components; 5 | use color_eyre::eyre::Result; 6 | 7 | pub fn run(maybe_language: Option<&languages::Language>, config: &config::Config) -> Result<()> { 8 | let bin_dir = config::bin_dir(); 9 | let _ = std::fs::create_dir_all(&bin_dir); 10 | let b = languages::bins(config); 11 | 12 | if let Some(language) = maybe_language { 13 | let bins = b 14 | .iter() 15 | .filter_map(|(a, b)| if *b == *language { Some(a) } else { None }); 16 | links::update(bins, &bin_dir) 17 | } else { 18 | let bins = b.iter().map(|(a, _)| a); 19 | links::update(bins, &bin_dir)?; 20 | 21 | // also update component links 22 | let bins = components::bins().into_iter().map(|(a, _)| a); 23 | links::update(bins, &bin_dir) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/components/rebar3.rs: -------------------------------------------------------------------------------- 1 | use crate::components::{release_dir, Component, Kind}; 2 | use crate::github::GithubRepo; 3 | use color_eyre::eyre::{Result, WrapErr}; 4 | use regex::Regex; 5 | 6 | const KIND_STRING: &str = "rebar3"; 7 | 8 | pub fn new_component(release: &str) -> Result { 9 | Ok(Component { 10 | kind: Kind::Rebar3, 11 | release_dir: release_dir(KIND_STRING.to_string(), &release.to_string())?, 12 | asset_prefix: asset_prefix()?, 13 | repo: get_github_repo(), 14 | bins: bins(), 15 | }) 16 | } 17 | 18 | fn asset_prefix() -> Result { 19 | Regex::new(KIND_STRING).wrap_err("Unable to create asset regex") 20 | } 21 | 22 | fn get_github_repo() -> GithubRepo { 23 | GithubRepo { 24 | org: "erlang".to_string(), 25 | repo: KIND_STRING.to_string(), 26 | } 27 | } 28 | 29 | fn bins() -> Vec<(String, Kind)> { 30 | vec![(KIND_STRING.to_string(), Kind::Rebar3)] 31 | } 32 | -------------------------------------------------------------------------------- /src/links.rs: -------------------------------------------------------------------------------- 1 | use color_eyre::{eyre::eyre, eyre::Result}; 2 | use std::path::Path; 3 | use std::path::PathBuf; 4 | 5 | pub fn update(bins: impl Iterator, bin_dir: &PathBuf) -> Result<()> 6 | where 7 | S: AsRef + AsRef, 8 | { 9 | let mut has_err = false; 10 | for b in bins { 11 | let link = Path::new(&bin_dir).join(b); 12 | let beamup_exe = std::env::current_exe().unwrap(); 13 | debug!("linking {:?} to {:?}", link, beamup_exe); 14 | let _ = std::fs::remove_file(&link); 15 | match std::fs::hard_link(&beamup_exe, &link) { 16 | Ok(()) => {} 17 | Err(e) => { 18 | has_err = true; 19 | error!("Failed to link {:?} to {:?}: {}", link, beamup_exe, e); 20 | } 21 | } 22 | } 23 | 24 | // TODO: should do a multi-report error instead of this 25 | if has_err { 26 | Err(eyre!("Some links failed to be created")) 27 | } else { 28 | Ok(()) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /.github/workflows/shelltests.yml: -------------------------------------------------------------------------------- 1 | name: Shelltests 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | jobs: 10 | run: 11 | strategy: 12 | matrix: 13 | os: ["ubuntu-latest", "macos-latest"] 14 | runs-on: ${{ matrix.os }} 15 | steps: 16 | - name: Checkout 17 | uses: actions/checkout@v4 18 | 19 | - name: Install just 20 | uses: extractions/setup-just@v1 21 | 22 | - name: Set up Homebrew 23 | id: set-up-homebrew 24 | uses: Homebrew/actions/setup-homebrew@master 25 | 26 | - name: Install shelltestrunner 27 | run: brew install shelltestrunner 28 | 29 | - name: Build beamup 30 | run: cargo build 31 | 32 | - name: Run Shelltests 33 | if: ${{ matrix.os == 'ubuntu-latest' }} 34 | run: just shelltests 35 | env: 36 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 37 | 38 | - name: Run MacOS ShellTests 39 | if: ${{ matrix.os == 'macos-latest' }} 40 | run: | 41 | just shelltests-macos 42 | env: 43 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 44 | -------------------------------------------------------------------------------- /src/languages/elixir.rs: -------------------------------------------------------------------------------- 1 | use crate::config; 2 | use crate::languages::Language; 3 | use color_eyre::eyre::{eyre, Result, WrapErr}; 4 | use regex::Regex; 5 | 6 | #[cfg(unix)] 7 | pub fn bins() -> Vec<(String, Language)> { 8 | vec![ 9 | ("elixir".to_string(), Language::Elixir), 10 | ("elixirc".to_string(), Language::Elixir), 11 | ("iex".to_string(), Language::Elixir), 12 | ("mix".to_string(), Language::Elixir), 13 | ] 14 | } 15 | 16 | #[cfg(windows)] 17 | pub fn bins() -> Vec<(String, Language)> { 18 | vec![ 19 | ("elixir.bat".to_string(), Language::Elixir), 20 | ("elixirc.bat".to_string(), Language::Elixir), 21 | ("iex.bat".to_string(), Language::Elixir), 22 | ("mix.bat".to_string(), Language::Elixir), 23 | ("mix.ps1".to_string(), Language::Elixir), 24 | ] 25 | } 26 | 27 | pub fn asset_prefix() -> Result { 28 | // find dir of active Erlang 29 | match config::get_otp_major_vsn() { 30 | Ok(otp_major_vsn) => Regex::new(format!("elixir-otp-{otp_major_vsn:}.zip").as_str()) 31 | .wrap_err("Unable to create regex for elixir asset"), 32 | Err(_) => Err(eyre!("No Erlang install found.")), 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /shelltests/installs.test: -------------------------------------------------------------------------------- 1 | # test help works 2 | target/debug/beamup help 3 | >>> /Manage BEAM language installs/ 4 | >>>= 0 5 | 6 | # test an install of gleam 7 | target/debug/beamup install gleam v1.4.1 8 | >>>=0 9 | 10 | # list available installs 11 | target/debug/beamup list 12 | >>> 13 | Elixir: 14 | 15 | Erlang: 16 | 17 | Gleam: 18 | v1.4.1 19 | >>>=0 20 | 21 | # test link was created in the right place 22 | ls ~/.local/bin/gleam || ls ~/.beamup/bin/gleam 23 | >>>=0 24 | 25 | # try installing the same version again and error 26 | target/debug/beamup install gleam v1.4.1 27 | >>>=1 28 | 29 | # force an install of the same version 30 | target/debug/beamup install -f gleam v1.4.1 31 | >>>=0 32 | 33 | # test attempt to install elixir without erlang 34 | target/debug/beamup install elixir latest 35 | >>>2 /No default Erlang installation found. Install an Erlang version, like `beamup install erlang latest` or set a default with `beamup default erlang ` first./ 36 | >>>=1 37 | 38 | # found a bug in update-links and that it wasn't run in tests so adding 39 | target/debug/beamup update-links 40 | >>>=0 41 | 42 | # test an install of erlang 43 | target/debug/beamup install erlang latest 44 | >>>=0 45 | 46 | # test an install of elixir 47 | target/debug/beamup install elixir latest 48 | >>>=0 49 | -------------------------------------------------------------------------------- /src/languages/gleam.rs: -------------------------------------------------------------------------------- 1 | use crate::languages::Language; 2 | use color_eyre::eyre::{eyre, Result, WrapErr}; 3 | use regex::Regex; 4 | 5 | #[cfg(unix)] 6 | pub fn bins() -> Vec<(String, Language)> { 7 | vec![("gleam".to_string(), Language::Gleam)] 8 | } 9 | 10 | #[cfg(windows)] 11 | pub fn bins() -> Vec<(String, Language)> { 12 | vec![ 13 | ("gleam.exe".to_string(), Language::Gleam), 14 | ("gleam".to_string(), Language::Gleam), 15 | ] 16 | } 17 | 18 | pub fn asset_prefix() -> Result { 19 | match (std::env::consts::ARCH, std::env::consts::OS) { 20 | ("x86_64", "linux") => Regex::new("gleam-.*-x86_64-unknown-linux-musl.tar.gz") 21 | .wrap_err("Unable to create asset regex"), 22 | ("aarch64", "linux") => Regex::new("gleam-.*-aarch64-unknown-linux-musl.tar.gz") 23 | .wrap_err("Unable to create asset regex"), 24 | ("x86_64", "macos") => Regex::new("gleam-.*-x86_64-apple-darwin.tar.gz") 25 | .wrap_err("Unable to create asset regex"), 26 | ("aarch64", "macos") => Regex::new("gleam-.*-aarch64-apple-darwin.tar.gz") 27 | .wrap_err("Unable to create asset regex"), 28 | ("x86_64", "windows") => Regex::new("gleam-.*-x86_64-pc-windows-msvc.zip") 29 | .wrap_err("Unable to create asset regex"), 30 | _ => Err(eyre!("Unknown architecture or OS for installing gleam")), 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/utils.rs: -------------------------------------------------------------------------------- 1 | use crate::eyre; 2 | use color_eyre::eyre::Result; 3 | use std::fs; 4 | use std::path::PathBuf; 5 | 6 | pub fn check_release_dir(release_dir: &PathBuf, force: bool) -> Result<()> { 7 | match release_dir.try_exists() { 8 | Ok(true) => 9 | match force { 10 | true => { 11 | Ok(()) 12 | }, 13 | _ => Err(eyre!("Install directory already exists. Use `-f true` to delete {:?} and recreate instead of giving this error.", release_dir)), 14 | } 15 | Ok(false) => Ok(()), 16 | Err(e) => Err(eyre!( 17 | "Unable to check for existence of install directory: {e:?}" 18 | )), 19 | } 20 | } 21 | 22 | pub fn maybe_create_release_dir(release_dir: &PathBuf, force: bool) -> Result<()> { 23 | match release_dir.try_exists() { 24 | Ok(true) => 25 | match force { 26 | true => { 27 | info!("Force enabled. Deleting existing release directory {:?}", release_dir); 28 | fs::remove_dir_all(release_dir)? 29 | }, 30 | _ => return Err(eyre!("Install directory already exists. Use `-f true` to delete {:?} and recreate instead of giving this error.", release_dir)), 31 | } 32 | Ok(false) => {}, 33 | Err(e) => return Err(eyre!( 34 | "Unable to check for existence of install directory: {e:?}" 35 | )), 36 | }; 37 | 38 | let _ = std::fs::create_dir_all(release_dir); 39 | 40 | Ok(()) 41 | } 42 | -------------------------------------------------------------------------------- /src/components.rs: -------------------------------------------------------------------------------- 1 | use clap::ValueEnum; 2 | pub mod elp; 3 | pub mod rebar3; 4 | use crate::config; 5 | use crate::github::GithubRepo; 6 | use color_eyre::eyre::Result; 7 | use std::path::PathBuf; 8 | use strum::IntoEnumIterator; 9 | 10 | #[derive(ValueEnum, Debug, Clone, PartialEq, EnumIter)] 11 | pub enum Kind { 12 | Elp, 13 | Rebar3, 14 | } 15 | 16 | impl std::fmt::Display for Kind { 17 | fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { 18 | match self { 19 | Kind::Elp => write!(f, "elp"), 20 | Kind::Rebar3 => write!(f, "rebar3"), 21 | } 22 | } 23 | } 24 | 25 | pub fn print() { 26 | for l in Kind::iter() { 27 | println!("{:?}", l); 28 | } 29 | } 30 | 31 | pub fn bins() -> Vec<(String, Kind)> { 32 | Kind::iter() 33 | .flat_map(|kind| { 34 | // TODO: Fix me, needs to be able to fail properly if data_local_dir fails 35 | let c = Component::new(kind.clone(), "").unwrap(); 36 | 37 | c.bins 38 | }) 39 | .collect() 40 | } 41 | 42 | pub struct Component { 43 | pub kind: Kind, 44 | pub release_dir: PathBuf, 45 | pub asset_prefix: regex::Regex, 46 | pub repo: GithubRepo, 47 | pub bins: Vec<(String, Kind)>, 48 | } 49 | 50 | impl Component { 51 | pub fn new(kind: Kind, release: &str) -> Result { 52 | match kind { 53 | Kind::Elp => elp::new_component(release), 54 | Kind::Rebar3 => rebar3::new_component(release), 55 | } 56 | } 57 | } 58 | 59 | pub fn release_dir(kind_str: String, id: &String) -> Result { 60 | let release_dir = config::data_dir()?.join("beamup").join(kind_str).join(id); 61 | 62 | Ok(release_dir) 63 | } 64 | -------------------------------------------------------------------------------- /src/components/elp.rs: -------------------------------------------------------------------------------- 1 | use crate::components::{release_dir, Component, Kind}; 2 | use crate::config; 3 | use crate::github::GithubRepo; 4 | use color_eyre::eyre::{Result, WrapErr}; 5 | use regex::Regex; 6 | 7 | const KIND_STRING: &str = "elp"; 8 | 9 | pub fn new_component(release: &str) -> Result { 10 | Ok(Component { 11 | kind: Kind::Elp, 12 | release_dir: release_dir(KIND_STRING.to_string(), &release.to_string())?, 13 | asset_prefix: asset_prefix()?, 14 | repo: get_github_repo(), 15 | bins: bins(), 16 | }) 17 | } 18 | 19 | fn bins() -> Vec<(String, Kind)> { 20 | vec![(KIND_STRING.to_string(), Kind::Elp)] 21 | } 22 | 23 | fn asset_prefix() -> Result { 24 | let otp_major_vsn = match config::get_otp_major_vsn() { 25 | Ok(otp_major_vsn) => otp_major_vsn, 26 | Err(_) => "".to_string(), 27 | }; 28 | 29 | match (std::env::consts::ARCH, std::env::consts::OS) { 30 | ("x86_64", "linux") => { 31 | Regex::new(format!("elp-linux-x86_64-unknown-linux-gnu-otp-{otp_major_vsn:}").as_str()) 32 | .wrap_err("Failed to create asset regex") 33 | } 34 | ("aarch64", "linux") => { 35 | Regex::new(format!("elp-linux-aarch64-unknown-linux-gnu-otp-{otp_major_vsn:}").as_str()) 36 | .wrap_err("Failed to create asset regex") 37 | } 38 | ("x86_64", "macos") => { 39 | Regex::new(format!("elp-macos-x86_64-apple-darwin-otp-{otp_major_vsn:}").as_str()) 40 | .wrap_err("Failed to create asset regex") 41 | } 42 | ("aarch64", "macos") => { 43 | Regex::new(format!("elp-macos-aarch64-apple-darwin-otp-{otp_major_vsn:}").as_str()) 44 | .wrap_err("Failed to create asset regex") 45 | } 46 | _ => { 47 | // TODO: maybe turn this into an Option type and return None 48 | Regex::new("").wrap_err("Failed to create asset regex") 49 | } 50 | } 51 | } 52 | 53 | fn get_github_repo() -> GithubRepo { 54 | GithubRepo { 55 | org: "WhatsApp".to_string(), 56 | repo: "erlang-language-platform".to_string(), 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "beamup" 3 | version = "0.8.0" 4 | description = " Install and control active BEAM languages and components" 5 | edition = "2021" 6 | authors = ["Tristan Sloughter "] 7 | repository = "https://github.com/tsloughter/beamup" 8 | license = "Apache-2.0" 9 | readme = "README.md" 10 | homepage = "https://github.com/tsloughter/beamup" 11 | 12 | [package.metadata.wix] 13 | upgrade-guid = "67FB3E2E-2204-4262-B187-C940B9E293E1" 14 | path-guid = "7600FDEA-F157-41B8-A433-A81FC6A057DF" 15 | license = false 16 | eula = false 17 | 18 | [dependencies] 19 | clap = {version = "4.5.8", features = ["derive", "color"]} 20 | clap_complete = "4.5.7" 21 | log = "0.4.21" 22 | regex = "1.12.2" 23 | env_logger = "0.11.3" 24 | tempdir = "0.3.4" 25 | tar = "0.4.5" 26 | glob = "0.3.1" 27 | toml = "0.8" 28 | serde = { version = "1.0", features = ["derive"] } 29 | dirs = "5.0.1" 30 | indicatif = { version = "0.17.8", features = ["futures", "tokio", "improved_unicode"] } 31 | console = "0.15.8" 32 | num_cpus = "1.8.0" 33 | shell-words = "1.0.0" 34 | octocrab = "0.47.1" 35 | strum = { version = "0.26", features = ["derive"] } 36 | tokio = { version = "1.17.0", features = ["macros", "rt-multi-thread"] } 37 | bytes = { version = "1.6.1", features = [] } 38 | flate2 = "1.0.30" 39 | color-eyre = "0.6.5" 40 | http = "1.1.0" 41 | ureq = "2.10.0" 42 | windows-sys = { version = "0.59.0", features = ["Win32_System", "Win32_System_Console"] } 43 | zip = "2.2.0" 44 | 45 | # The profile that 'cargo dist' will build with 46 | [profile.dist] 47 | inherits = "release" 48 | lto = "thin" 49 | 50 | # Config for 'dist' 51 | [workspace.metadata.dist] 52 | # The preferred dist version to use in CI (Cargo.toml SemVer syntax) 53 | cargo-dist-version = "0.30.2" 54 | # CI backends to support 55 | ci = "github" 56 | # The installers to generate for each app 57 | installers = ["shell", "powershell", "msi"] 58 | # Target platforms to build apps for (Rust target-triple syntax) 59 | targets = ["aarch64-apple-darwin", "aarch64-unknown-linux-gnu", "aarch64-unknown-linux-musl", "x86_64-apple-darwin", "x86_64-unknown-linux-gnu", "x86_64-unknown-linux-musl", "x86_64-pc-windows-msvc"] 60 | # Which actions to run on pull requests 61 | pr-run-mode = "plan" 62 | # Path that installers should place binaries in 63 | install-path = ["$BEAMUP_HOME/bin", "~/.local/bin"] 64 | # Whether to install an updater program 65 | install-updater = false 66 | 67 | [workspace.metadata.dist.github-custom-runners] 68 | aarch64-unknown-linux-gnu = "buildjet-2vcpu-ubuntu-2204-arm" 69 | aarch64-unknown-linux-musl = "buildjet-2vcpu-ubuntu-2204-arm" 70 | -------------------------------------------------------------------------------- /src/cmd/component_install.rs: -------------------------------------------------------------------------------- 1 | use crate::components; 2 | use crate::github; 3 | use crate::utils; 4 | use color_eyre::{eyre::Report, eyre::Result, eyre::WrapErr}; 5 | use flate2::read::GzDecoder; 6 | use std::fs; 7 | use std::path::PathBuf; 8 | use tar::Archive; 9 | use tempdir::TempDir; 10 | use zip; 11 | 12 | pub fn run( 13 | c: &components::Component, 14 | release: &String, 15 | force: bool, 16 | ) -> Result { 17 | utils::check_release_dir(&c.release_dir, force)?; 18 | let release_dir_string = c 19 | .release_dir 20 | .clone() 21 | .into_os_string() 22 | .into_string() 23 | .unwrap(); 24 | let asset_name = &c.asset_prefix; 25 | let github_repo = &c.repo; 26 | let out_dir = TempDir::new(github_repo.repo.as_str())?; 27 | let file = github::download_asset(asset_name, out_dir.path(), github_repo, release)?; 28 | debug!("file {:?} downloaded", file); 29 | let open_file = fs::File::open(&file).wrap_err_with(|| { 30 | format!( 31 | "Downloaded Github asset for release {} into file {:?} not found", 32 | release, &file 33 | ) 34 | })?; 35 | 36 | utils::maybe_create_release_dir(&c.release_dir, force)?; 37 | 38 | // TODO: better ways to check the type than the extension 39 | let ext = file.extension().map_or("", |e| e.to_str().unwrap_or("")); 40 | match ext { 41 | "zip" => { 42 | let mut archive = zip::ZipArchive::new(open_file)?; 43 | let release_dir = match c.kind { 44 | components::Kind::Elp => c.release_dir.join("bin"), 45 | _ => c.release_dir.clone(), 46 | }; 47 | archive.extract(&release_dir)?; 48 | Ok(release_dir_string) 49 | } 50 | "gz" => { 51 | let tar = GzDecoder::new(open_file); 52 | let mut archive = Archive::new(tar); 53 | archive.unpack(&c.release_dir.join("bin"))?; 54 | Ok(release_dir_string) 55 | } 56 | _ => { 57 | // no unpacking needed, just copy to bin dir and make sure its executable 58 | let install_file = &c.release_dir.join("bin").join(file.file_name().unwrap()); 59 | let _ = std::fs::create_dir_all(c.release_dir.join("bin")); 60 | fs::copy(&file, install_file).wrap_err_with(|| { 61 | format!( 62 | "Failed to copy {} to {}", 63 | file.display(), 64 | install_file.display() 65 | ) 66 | })?; 67 | 68 | set_permissions(install_file)?; 69 | 70 | Ok(release_dir_string) 71 | } 72 | } 73 | } 74 | 75 | #[cfg(unix)] 76 | fn set_permissions(to: &PathBuf) -> Result<()> { 77 | use std::os::unix::fs::PermissionsExt; 78 | 79 | let executable_permissions = PermissionsExt::from_mode(0o744); 80 | 81 | let to_file = fs::File::open(to)?; 82 | to_file.set_permissions(executable_permissions)?; 83 | 84 | Ok(()) 85 | } 86 | 87 | #[cfg(windows)] 88 | fn set_permissions(_to: &PathBuf) -> Result<()> { 89 | Ok(()) 90 | } 91 | -------------------------------------------------------------------------------- /src/cmd/install.rs: -------------------------------------------------------------------------------- 1 | use crate::github; 2 | use crate::languages; 3 | use crate::languages::Libc; 4 | use crate::utils; 5 | use color_eyre::eyre::{eyre, Report, Result, WrapErr}; 6 | use flate2::read::GzDecoder; 7 | use std::fs::File; 8 | use std::path::PathBuf; 9 | use tar::Archive; 10 | use tempdir::TempDir; 11 | use zip; 12 | 13 | #[cfg(windows)] 14 | use std::process::ExitStatus; 15 | 16 | pub fn run( 17 | installable: &T, 18 | id: &String, 19 | release: &str, 20 | libc: &Option, 21 | force: bool, 22 | ) -> Result { 23 | let release_dir = &installable.release_dir(id)?; 24 | utils::maybe_create_release_dir(&release_dir, force)?; 25 | 26 | let github_repo = installable.binary_repo(); 27 | let out_dir = TempDir::new(github_repo.repo.as_str())?; 28 | let asset_name = installable.asset_prefix(libc)?; 29 | let file = github::download_asset(&asset_name, out_dir.path(), &github_repo, release)?; 30 | debug!("file {:?} downloaded", file); 31 | let open_file = File::open(&file).wrap_err_with(|| { 32 | format!( 33 | "Downloaded Github asset for release {} into file {:?} not found", 34 | release, &file 35 | ) 36 | })?; 37 | 38 | let extract_dir = installable.extract_dir(id)?; 39 | 40 | // TODO: better ways to check the type than the extension 41 | let ext = file.extension().map_or("", |e| e.to_str().unwrap_or("")); 42 | match ext { 43 | "exe" => { 44 | let release_dir = release_dir.clone().into_os_string().into_string().unwrap(); 45 | exe_run(file, release_dir.clone())?; 46 | Ok(release_dir) 47 | } 48 | "zip" => { 49 | let mut archive = zip::ZipArchive::new(open_file)?; 50 | archive.extract(extract_dir)?; 51 | Ok(release_dir.clone().into_os_string().into_string().unwrap()) 52 | } 53 | _ => { 54 | let tar = GzDecoder::new(open_file); 55 | let mut archive = Archive::new(tar); 56 | archive.unpack(extract_dir)?; 57 | Ok(release_dir.clone().into_os_string().into_string().unwrap()) 58 | } 59 | } 60 | } 61 | 62 | #[cfg(unix)] 63 | fn exe_run(_cmd: PathBuf, _release_dir: String) -> Result<(), Report> { 64 | Err(eyre!( 65 | "Attempted to execute a Windows exeutable on a non-Windows system" 66 | )) 67 | } 68 | 69 | // thanks rustup command.rs 70 | #[cfg(windows)] 71 | fn exe_run(file: PathBuf, release_dir: String) -> Result { 72 | use std::os::windows::process::CommandExt; 73 | use std::process::Command; 74 | use windows_sys::Win32::Foundation::{BOOL, FALSE, TRUE}; 75 | use windows_sys::Win32::System::Console::SetConsoleCtrlHandler; 76 | 77 | unsafe extern "system" fn ctrlc_handler(_: u32) -> BOOL { 78 | // Do nothing. Let the child process handle it. 79 | TRUE 80 | } 81 | unsafe { 82 | if SetConsoleCtrlHandler(Some(ctrlc_handler), TRUE) == FALSE { 83 | return Err(eyre!("Unable to set console handler",)); 84 | } 85 | } 86 | 87 | let mut binding = Command::new(file); 88 | let cmd = binding.raw_arg(&format!("/S /D={release_dir:}")); 89 | debug!("Command being run: {cmd:?}"); 90 | 91 | Ok(cmd.status()?) 92 | } 93 | -------------------------------------------------------------------------------- /src/run.rs: -------------------------------------------------------------------------------- 1 | use crate::components; 2 | use crate::config; 3 | use color_eyre::eyre::eyre; 4 | use color_eyre::eyre::Result; 5 | use std::env; 6 | use std::env::{join_paths, split_paths, Args}; 7 | use std::path::*; 8 | use std::process::Command; 9 | 10 | pub fn run_component(bin: &str, kind: &components::Kind, args: Args) -> Result<()> { 11 | // no -c argument available in this case 12 | let dir = config::component_install_to_use(kind)?; 13 | let cmd = Path::new(bin); 14 | 15 | debug!("running component {:?}", cmd); 16 | debug!("running with args {:?}", args); 17 | 18 | let path = env::var("PATH")?; 19 | let mut paths = split_paths(&path).collect::>(); 20 | 21 | let install_bin_dir = Path::new(&dir).join("bin"); 22 | if install_bin_dir.is_dir() { 23 | paths.insert(0, Path::new(&dir).join("bin")); 24 | let new_path = join_paths(paths)?; 25 | 26 | let mut binding = Command::new(cmd); 27 | let binding = binding.env("PATH", &new_path); 28 | let cmd = binding.args(args); 29 | 30 | exec(cmd) 31 | } else { 32 | return Err(eyre!( 33 | "Directory of component expected install does not exist: {:?} ", 34 | install_bin_dir 35 | )); 36 | } 37 | } 38 | 39 | pub fn run(bin: &str, args: Args) -> Result<()> { 40 | // no -c argument available in this case 41 | let dir = config::install_to_use_by_bin(bin)?; 42 | let cmd = Path::new(bin); 43 | 44 | debug!("running language command {:?}", cmd); 45 | debug!("running with args {:?}", args); 46 | debug!("running language cmd {:?}", dir); 47 | let path = env::var("PATH")?; 48 | 49 | let mut paths = split_paths(&path).collect::>(); 50 | 51 | let install_bin_dir = Path::new(&dir).join("bin"); 52 | 53 | if install_bin_dir.is_dir() { 54 | paths.insert(0, install_bin_dir); 55 | let new_path = join_paths(paths)?; 56 | 57 | let mut binding = Command::new(cmd); 58 | let binding = binding.env("PATH", &new_path); 59 | let cmd = binding.args(args); 60 | debug!("running language cmd {:?}", cmd); 61 | exec(cmd) 62 | } else { 63 | return Err(eyre!( 64 | "Directory of expected install does not exist: {:?} ", 65 | install_bin_dir 66 | )); 67 | } 68 | } 69 | 70 | #[cfg(unix)] 71 | fn exec(cmd: &mut Command) -> Result<()> { 72 | use std::os::unix::prelude::*; 73 | Err(cmd.exec().into()) 74 | } 75 | 76 | // thanks rustup command.rs 77 | #[cfg(windows)] 78 | fn exec(cmd: &mut Command) -> Result<()> { 79 | use color_eyre::eyre::eyre; 80 | use windows_sys::Win32::Foundation::{BOOL, FALSE, TRUE}; 81 | use windows_sys::Win32::System::Console::SetConsoleCtrlHandler; 82 | 83 | unsafe extern "system" fn ctrlc_handler(_: u32) -> BOOL { 84 | // Do nothing. Let the child process handle it. 85 | TRUE 86 | } 87 | unsafe { 88 | if SetConsoleCtrlHandler(Some(ctrlc_handler), TRUE) == FALSE { 89 | return Err(eyre!("Unable to set console handler",)); 90 | } 91 | } 92 | 93 | let status = cmd.status()?; 94 | if !status.success() { 95 | std::process::exit(status.code().unwrap_or(0)) 96 | } 97 | 98 | Ok(()) 99 | } 100 | -------------------------------------------------------------------------------- /cliff.toml: -------------------------------------------------------------------------------- 1 | # git-cliff ~ configuration file 2 | # https://git-cliff.org/docs/configuration 3 | 4 | [changelog] 5 | # template for the changelog footer 6 | header = """ 7 | # Changelog\n 8 | All notable changes to this project will be documented in this file. 9 | 10 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 11 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).\n 12 | """ 13 | # template for the changelog body 14 | # https://keats.github.io/tera/docs/#introduction 15 | body = """ 16 | {% if version -%} 17 | ## [{{ version | trim_start_matches(pat="v") }}] - {{ timestamp | date(format="%Y-%m-%d") }} 18 | {% else -%} 19 | ## [Unreleased] 20 | {% endif -%} 21 | {% for group, commits in commits | group_by(attribute="group") %} 22 | ### {{ group | upper_first }} 23 | {% for commit in commits %} 24 | - {{ commit.message | upper_first }}\ 25 | {% endfor %} 26 | {% endfor %}\n 27 | """ 28 | # template for the changelog footer 29 | footer = """ 30 | {% for release in releases -%} 31 | {% if release.version -%} 32 | {% if release.previous.version -%} 33 | [{{ release.version | trim_start_matches(pat="v") }}]: \ 34 | https://github.com/{{ remote.github.owner }}/{{ remote.github.repo }}\ 35 | /compare/{{ release.previous.version }}..{{ release.version }} 36 | {% endif -%} 37 | {% else -%} 38 | [unreleased]: https://github.com/{{ remote.github.owner }}/{{ remote.github.repo }}\ 39 | /compare/{{ release.previous.version }}..HEAD 40 | {% endif -%} 41 | {% endfor %} 42 | 43 | """ 44 | # remove the leading and trailing whitespace from the templates 45 | trim = true 46 | 47 | [git] 48 | # parse the commits based on https://www.conventionalcommits.org 49 | conventional_commits = true 50 | # filter out the commits that are not conventional 51 | filter_unconventional = true 52 | # process each line of a commit as an individual commit 53 | split_commits = false 54 | # regex for parsing and grouping commits 55 | commit_parsers = [ 56 | { message = "^feat", group = "Added" }, 57 | { message = "^chore\\(release\\): prepare for", skip = true }, 58 | { message = "^chore\\(deps.*\\)", skip = true }, 59 | { message = "^chore\\(pr\\)", skip = true }, 60 | { message = "^chore\\(pull\\)", skip = true }, 61 | { message = "^chore|^ci", skip = true}, 62 | { message = "^.*: add", group = "Added" }, 63 | { message = "^.*: support", group = "Added" }, 64 | { message = "^.*: remove", group = "Removed" }, 65 | { message = "^.*: delete", group = "Removed" }, 66 | { message = "^test", group = "Fixed" }, 67 | { message = "^fix", group = "Fixed" }, 68 | { message = "^.*: fix", group = "Fixed" }, 69 | { message = "^.*", group = "Changed" }, 70 | ] 71 | # protect breaking changes from being skipped due to matching a skipping commit_parser 72 | protect_breaking_commits = false 73 | # filter out the commits that are not matched by commit parsers 74 | filter_commits = true 75 | # regex for matching git tags 76 | tag_pattern = "v[0-9].*" 77 | # regex for skipping tags 78 | # skip_tags = "v0.1.0-beta.1" 79 | # regex for ignoring tags 80 | ignore_tags = "" 81 | # sort the tags topologically 82 | topo_order = false 83 | # sort the commits inside sections by oldest/newest order 84 | sort_commits = "oldest" 85 | -------------------------------------------------------------------------------- /src/languages/erlang.rs: -------------------------------------------------------------------------------- 1 | use crate::languages::{Language, Libc}; 2 | use color_eyre::eyre::{eyre, Result, WrapErr}; 3 | use regex::Regex; 4 | 5 | #[cfg(unix)] 6 | pub fn bins() -> Vec<(String, Language)> { 7 | vec![ 8 | ("ct_run".to_string(), Language::Erlang), 9 | ("dialyzer".to_string(), Language::Erlang), 10 | ("epmd".to_string(), Language::Erlang), 11 | ("erl".to_string(), Language::Erlang), 12 | ("erlc".to_string(), Language::Erlang), 13 | ("erl_call".to_string(), Language::Erlang), 14 | ("escript".to_string(), Language::Erlang), 15 | ("run_erl".to_string(), Language::Erlang), 16 | ("run_test".to_string(), Language::Erlang), 17 | ("to_erl".to_string(), Language::Erlang), 18 | ("typer".to_string(), Language::Erlang), 19 | ] 20 | } 21 | 22 | #[cfg(windows)] 23 | pub fn bins() -> Vec<(String, Language)> { 24 | vec![ 25 | ("ct_run.exe".to_string(), Language::Erlang), 26 | ("dialyzer.exe".to_string(), Language::Erlang), 27 | ("epmd.exe".to_string(), Language::Erlang), 28 | ("erl.exe".to_string(), Language::Erlang), 29 | ("erlc.exe".to_string(), Language::Erlang), 30 | ("erl_call.exe".to_string(), Language::Erlang), 31 | ("escript.exe".to_string(), Language::Erlang), 32 | ("run_erl.exe".to_string(), Language::Erlang), 33 | ("run_test.exe".to_string(), Language::Erlang), 34 | ("to_erl.exe".to_string(), Language::Erlang), 35 | ("typer.exe".to_string(), Language::Erlang), 36 | ("ct_run".to_string(), Language::Erlang), 37 | ("dialyzer".to_string(), Language::Erlang), 38 | ("epmd".to_string(), Language::Erlang), 39 | ("erl".to_string(), Language::Erlang), 40 | ("erlc".to_string(), Language::Erlang), 41 | ("erl_call".to_string(), Language::Erlang), 42 | ("escript".to_string(), Language::Erlang), 43 | ("run_erl".to_string(), Language::Erlang), 44 | ("run_test".to_string(), Language::Erlang), 45 | ("to_erl".to_string(), Language::Erlang), 46 | ("typer".to_string(), Language::Erlang), 47 | ] 48 | } 49 | 50 | pub fn asset_prefix(libc: &Option) -> Result { 51 | let libc = match libc { 52 | None => "", 53 | Some(Libc::Glibc) => "-glibc", 54 | Some(Libc::Musl) => "-musl", 55 | }; 56 | match (std::env::consts::ARCH, std::env::consts::OS) { 57 | ("x86", "windows") => { 58 | Regex::new("otp_win32_.*.exe").wrap_err("Unable to create asset regex") 59 | } 60 | ("x86_64", "windows") => { 61 | Regex::new("otp_win64_.*.exe").wrap_err("Unable to create asset regex") 62 | } 63 | ("x86_64", "macos") => { 64 | Regex::new("otp-x86_64-apple-darwin.tar.gz").wrap_err("Unable to create asset regex") 65 | } 66 | ("aarch64", "macos") => { 67 | Regex::new("otp-aarch64-apple-darwin.tar.gz").wrap_err("Unable to create asset regex") 68 | } 69 | ("aarch64", "linux") => Regex::new(format!("erlang-.*-arm64{libc}.tar.gz").as_str()) 70 | .wrap_err("Unable to create asset regex"), 71 | ("x86_64", "linux") => Regex::new(format!("erlang-.*-x64{libc}.tar.gz").as_str()) 72 | .wrap_err("Unable to create asset regex"), 73 | _ => Err(eyre!("Unknown architecture or OS for installing Erlang")), 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## [0.8.0] - 2025-11-24 9 | 10 | ### Added 11 | 12 | - Support latest tag for gleam and erlang static builds 13 | 14 | ### Fixed 15 | 16 | - Only call asset_prefix in install where it is needed 17 | 18 | ## [0.7.2] - 2025-11-21 19 | 20 | ### Fixed 21 | 22 | - Check for an installed erlang before trying to install elixir 23 | 24 | ## [0.7.1] - 2025-11-21 25 | 26 | ### Fixed 27 | 28 | - Don't crash when running releases command on elixir 29 | 30 | ## [0.7.0] - 2025-11-21 31 | 32 | ### Added 33 | 34 | - Install static binary builds of erlang on linux 35 | 36 | ## [0.6.0] - 2024-09-26 37 | 38 | ### Added 39 | 40 | - Support binary install of erlang on macos 41 | 42 | ### Fixed 43 | 44 | - Search with .exe and without in windows for bins 45 | 46 | ## [0.5.0] - 2024-09-15 47 | 48 | ### Added 49 | 50 | - Components support for installing elp and rebar3 51 | 52 | ### Fixed 53 | 54 | - Allow -f/--force without passing the true/false value 55 | 56 | ## [0.4.7] - 2024-09-02 57 | 58 | ### Fixed 59 | 60 | - Make finding active otp major vsn less hacky 61 | 62 | ## [0.4.6] - 2024-09-02 63 | 64 | ### Added 65 | 66 | - Add musl on top of gnu builds to aarch64 linux releases 67 | 68 | ## [0.4.5] - 2024-09-02 69 | 70 | ### Fixed 71 | 72 | - Don't append bin/ path to gleam install dir 73 | 74 | ## [0.4.4] - 2024-09-02 75 | 76 | ### Fixed 77 | 78 | - Languages command to print all languages 79 | 80 | ## [0.4.3] - 2024-08-31 81 | 82 | ### Added 83 | 84 | - Better error message when installing Elixir without Erlang 85 | 86 | ## [0.4.2] - 2024-08-31 87 | 88 | ### Fixed 89 | 90 | - Do not attempt to install docs for now 91 | 92 | ## [0.4.1] - 2024-08-29 93 | 94 | ### Added 95 | 96 | - Add Elixir ids to the list output 97 | 98 | ## [0.4.0] - 2024-08-28 99 | 100 | ### Added 101 | 102 | - Add support for the 'list' command 103 | - Support installing Elixir on Windows/Linux/Mac 104 | - Add OTP vsn Elixir was built for in install metadata 105 | - Include release/git_ref in install metadata 106 | 107 | ### Removed 108 | 109 | - Remove link checker in github pages site 110 | 111 | ## [0.3.0] - 2024-08-26 112 | 113 | ### Added 114 | 115 | - Windows gleam install and run support 116 | - Windows Erlang install and run support 117 | - Add support for forcing the overwrite of an existing install 118 | 119 | ### Fixed 120 | 121 | - Error if user tries to run Erlang build on windows 122 | - Error out if update_links fails 123 | 124 | ## [0.2.0] - 2024-08-19 125 | 126 | ### Added 127 | 128 | - Support default build options in config and env var 129 | 130 | ## [0.1.0] - 2024-08-17 131 | 132 | [0.8.0]: https://github.com/tsloughter/beamup/compare/v0.7.2..v0.8.0 133 | [0.7.2]: https://github.com/tsloughter/beamup/compare/v0.7.1..v0.7.2 134 | [0.7.1]: https://github.com/tsloughter/beamup/compare/v0.7.0..v0.7.1 135 | [0.7.0]: https://github.com/tsloughter/beamup/compare/v0.6.0..v0.7.0 136 | [0.6.0]: https://github.com/tsloughter/beamup/compare/v0.5.0..v0.6.0 137 | [0.5.0]: https://github.com/tsloughter/beamup/compare/v0.4.7..v0.5.0 138 | [0.4.7]: https://github.com/tsloughter/beamup/compare/v0.4.6..v0.4.7 139 | [0.4.6]: https://github.com/tsloughter/beamup/compare/v0.4.5..v0.4.6 140 | [0.4.5]: https://github.com/tsloughter/beamup/compare/v0.4.4..v0.4.5 141 | [0.4.4]: https://github.com/tsloughter/beamup/compare/v0.4.3..v0.4.4 142 | [0.4.3]: https://github.com/tsloughter/beamup/compare/v0.4.2..v0.4.3 143 | [0.4.2]: https://github.com/tsloughter/beamup/compare/v0.4.1..v0.4.2 144 | [0.4.1]: https://github.com/tsloughter/beamup/compare/v0.4.0..v0.4.1 145 | [0.4.0]: https://github.com/tsloughter/beamup/compare/v0.3.0..v0.4.0 146 | [0.3.0]: https://github.com/tsloughter/beamup/compare/v0.2.0..v0.3.0 147 | [0.2.0]: https://github.com/tsloughter/beamup/compare/v0.1.2..v0.2.0 148 | 149 | 150 | -------------------------------------------------------------------------------- /.github/workflows/web.yml: -------------------------------------------------------------------------------- 1 | # Workflow to build your docs with oranda (and mdbook) 2 | # and deploy them to Github Pages 3 | name: Web 4 | 5 | # We're going to push to the gh-pages branch, so we need that permission 6 | permissions: 7 | contents: write 8 | 9 | # What situations do we want to build docs in? 10 | # All of these work independently and can be removed / commented out 11 | # if you don't want oranda/mdbook running in that situation 12 | on: 13 | # Check that a PR didn't break docs! 14 | # 15 | # Note that the "Deploy to Github Pages" step won't run in this mode, 16 | # so this won't have any side-effects. But it will tell you if a PR 17 | # completely broke oranda/mdbook. Sadly we don't provide previews (yet)! 18 | pull_request: 19 | 20 | # Whenever something gets pushed to main, update the docs! 21 | # This is great for getting docs changes live without cutting a full release. 22 | # 23 | # Note that if you're using cargo-dist, this will "race" the Release workflow 24 | # that actually builds the Github Release that oranda tries to read (and 25 | # this will almost certainly complete first). As a result you will publish 26 | # docs for the latest commit but the oranda landing page won't know about 27 | # the latest release. The workflow_run trigger below will properly wait for 28 | # cargo-dist, and so this half-published state will only last for ~10 minutes. 29 | # 30 | # If you only want docs to update with releases, disable this, or change it to 31 | # a "release" branch. You can, of course, also manually trigger a workflow run 32 | # when you want the docs to update. 33 | push: 34 | branches: 35 | - main 36 | 37 | # Whenever a workflow called "Release" completes, update the docs! 38 | # 39 | # If you're using cargo-dist, this is recommended, as it will ensure that 40 | # oranda always sees the latest release right when it's available. Note 41 | # however that Github's UI is wonky when you use workflow_run, and won't 42 | # show this workflow as part of any commit. You have to go to the "actions" 43 | # tab for your repo to see this one running (the gh-pages deploy will also 44 | # only show up there). 45 | workflow_run: 46 | workflows: [ "Release" ] 47 | types: 48 | - completed 49 | 50 | # Alright, let's do it! 51 | jobs: 52 | web: 53 | name: Build and deploy site and docs 54 | runs-on: ubuntu-latest 55 | steps: 56 | # Setup 57 | - uses: actions/checkout@v3 58 | with: 59 | fetch-depth: 0 60 | - uses: dtolnay/rust-toolchain@stable 61 | - uses: swatinem/rust-cache@v2 62 | 63 | # If you use any mdbook plugins, here's the place to install them! 64 | 65 | # Install and run oranda (and mdbook)! 66 | # 67 | # This will write all output to ./public/ (including copying mdbook's output to there). 68 | - name: Install and run oranda 69 | run: | 70 | curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/oranda/releases/latest/download/oranda-installer.sh | sh 71 | oranda build 72 | 73 | # - name: Prepare HTML for link checking 74 | # # untitaker/hyperlink supports no site prefixes, move entire site into 75 | # # a subfolder 76 | # run: mkdir /tmp/public/ && cp -R public /tmp/public/oranda 77 | 78 | # - name: Check HTML for broken internal links 79 | # uses: untitaker/hyperlink@0.1.29 80 | # with: 81 | # args: /tmp/public/ 82 | 83 | # Deploy to our gh-pages branch (creating it if it doesn't exist). 84 | # The "public" dir that oranda made above will become the root dir 85 | # of this branch. 86 | # 87 | # Note that once the gh-pages branch exists, you must 88 | # go into repo's settings > pages and set "deploy from branch: gh-pages". 89 | # The other defaults work fine. 90 | - name: Deploy to Github Pages 91 | uses: JamesIves/github-pages-deploy-action@v4.4.1 92 | # ONLY if we're on main (so no PRs or feature branches allowed!) 93 | if: ${{ github.ref == 'refs/heads/main' }} 94 | with: 95 | branch: gh-pages 96 | # Gotta tell the action where to find oranda's output 97 | folder: public 98 | token: ${{ secrets.GITHUB_TOKEN }} 99 | single-commit: true 100 | -------------------------------------------------------------------------------- /src/languages.rs: -------------------------------------------------------------------------------- 1 | use crate::config; 2 | use crate::github::GithubRepo; 3 | use crate::languages; 4 | use clap::ValueEnum; 5 | use color_eyre::eyre::{eyre, Result}; 6 | use std::path::PathBuf; 7 | use strum::IntoEnumIterator; 8 | pub mod elixir; 9 | pub mod erlang; 10 | pub mod gleam; 11 | 12 | #[derive(ValueEnum, Debug, Clone, PartialEq, EnumIter)] 13 | pub enum Libc { 14 | Glibc, 15 | Musl, 16 | } 17 | 18 | #[derive(ValueEnum, Debug, Clone, PartialEq, EnumIter)] 19 | pub enum Language { 20 | Elixir, 21 | Erlang, 22 | Gleam, 23 | } 24 | 25 | pub fn print() { 26 | for l in Language::iter() { 27 | println!("{:?}", l); 28 | } 29 | } 30 | 31 | impl std::fmt::Display for Language { 32 | fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { 33 | match self { 34 | Language::Erlang => write!(f, "erlang"), 35 | Language::Gleam => write!(f, "gleam"), 36 | Language::Elixir => write!(f, "elixir"), 37 | } 38 | } 39 | } 40 | 41 | pub fn bins(_config: &config::Config) -> Vec<(String, Language)> { 42 | let mut bins = vec![]; 43 | let mut elixir_bins = elixir::bins(); 44 | let mut erlang_bins = erlang::bins(); 45 | let mut gleam_bins = gleam::bins(); 46 | 47 | bins.append(&mut elixir_bins); 48 | bins.append(&mut erlang_bins); 49 | bins.append(&mut gleam_bins); 50 | 51 | bins 52 | } 53 | 54 | pub trait Installable { 55 | fn default_build_options(&self, config: &config::Config) -> String; 56 | 57 | fn binary_repo(&self) -> GithubRepo; 58 | fn source_repo(&self) -> GithubRepo; 59 | 60 | fn release_dir(&self, id: &String) -> Result; 61 | fn extract_dir(&self, id: &String) -> Result; 62 | 63 | fn asset_prefix(&self, libc: &Option) -> Result; 64 | } 65 | 66 | impl Installable for Language { 67 | fn default_build_options(&self, config: &config::Config) -> String { 68 | config::lookup_default_build_options(self, config) 69 | } 70 | 71 | fn binary_repo(&self) -> GithubRepo { 72 | match self { 73 | Language::Elixir => GithubRepo { 74 | org: "elixir-lang".to_string(), 75 | repo: "elixir".to_string(), 76 | }, 77 | Language::Erlang => match std::env::consts::OS { 78 | "windows" => GithubRepo { 79 | org: "erlang".to_string(), 80 | repo: "otp".to_string(), 81 | }, 82 | "macos" => GithubRepo { 83 | org: "erlef".to_string(), 84 | repo: "otp_builds".to_string(), 85 | }, 86 | _ => GithubRepo { 87 | org: "gleam-community".to_string(), 88 | repo: "erlang-linux-builds".to_string(), 89 | }, 90 | }, 91 | Language::Gleam => GithubRepo { 92 | org: "gleam-lang".to_string(), 93 | repo: "gleam".to_string(), 94 | }, 95 | } 96 | } 97 | 98 | fn source_repo(&self) -> GithubRepo { 99 | match self { 100 | Language::Elixir => GithubRepo { 101 | org: "elixir-lang".to_string(), 102 | repo: "elixir".to_string(), 103 | }, 104 | Language::Erlang => GithubRepo { 105 | org: "erlang".to_string(), 106 | repo: "otp".to_string(), 107 | }, 108 | Language::Gleam => GithubRepo { 109 | org: "gleam-lang".to_string(), 110 | repo: "gleam".to_string(), 111 | }, 112 | } 113 | } 114 | 115 | fn release_dir(&self, id: &String) -> Result { 116 | languages::release_dir(self.to_string(), id) 117 | } 118 | 119 | fn extract_dir(&self, id: &String) -> Result { 120 | languages::release_dir(self.to_string(), id) 121 | } 122 | 123 | fn asset_prefix(&self, libc: &Option) -> Result { 124 | match self { 125 | Language::Elixir => elixir::asset_prefix(), 126 | Language::Erlang => erlang::asset_prefix(libc), 127 | Language::Gleam => gleam::asset_prefix(), 128 | } 129 | } 130 | } 131 | 132 | pub fn bin_to_language(bin: String, config: &config::Config) -> Result { 133 | match bins(config).iter().find(|&(k, _)| *k == bin) { 134 | Some((_, language)) => Ok(language.clone()), 135 | _ => Err(eyre!("No language to run command {bin} for found")), 136 | } 137 | } 138 | 139 | pub fn release_dir(language_str: String, id: &String) -> Result { 140 | let release_dir = config::data_dir()? 141 | .join("beamup") 142 | .join(language_str) 143 | .join(id); 144 | 145 | Ok(release_dir) 146 | } 147 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # BEAMUp 2 | 3 | [![Release](https://github.com/tsloughter/beamup/actions/workflows/release.yml/badge.svg)](https://github.com/tsloughter/beamup/actions/workflows/release.yml) 4 | 5 |

6 | beamup logo 7 |

8 | 9 | A tool for installing languages (support for Gleam, Erlang and Elixir) that run 10 | on the [Erlang VM](https://www.erlang.org/) (BEAM) and related components -- 11 | component support to come in the future. 12 | 13 | ## Install 14 | 15 | An install script is provided for both Linux/Mac: 16 | 17 | ``` 18 | curl --proto '=https' --tlsv1.2 -LsSf https://github.com/tsloughter/beamup/releases/download/v0.8.0/beamup-installer.sh | sh 19 | ``` 20 | 21 | And Windows Powershell: 22 | 23 | ``` 24 | powershell -c "irm 25 | https://github.com/tsloughter/beamup/releases/download/v0.8.0/beamup-installer.ps1 26 | | iex" 27 | ``` 28 | 29 | Binaries can also be downloaded from the [releases on 30 | Github](https://github.com/tsloughter/beamup/releases). Or install from source 31 | using [cargo](https://doc.rust-lang.org/cargo/). 32 | 33 | ## Usage 34 | 35 | `beamup` will store configuration at: 36 | 37 | - Linux: `~/.config/beamup/config.toml` 38 | - Mac: `~/Library/Application Support/beamup/config.toml` 39 | - Windows: `~\AppData\Local\beamup\config.toml` 40 | 41 | Local configuration to set a language/component to use in a specific directory 42 | is in `./.beamup.toml`. 43 | 44 | Hard links to the `beamup` executable for each language command, i.e. `gleam`, 45 | `erlc`, `erl`, `iex`, etc, is created in the following directory: 46 | 47 | - Linux: `$XDG_BIN_HOME` or `$XDG_DATA_HOME/../bin` or `$HOME/.local/bin` 48 | - Mac: `~/.beamup/bin` 49 | - Windows: `~\.beamup\bin` 50 | 51 | This directory must be added to your `PATH` for `beamup` installs to work. 52 | 53 | Installs are currently done to the applications data directory. This defaults 54 | to: 55 | 56 | - Linux: `~/.local/share/beamup//` 57 | - Mac: `~/Library/Application Support/beamup//` 58 | - Windows: `~\AppData\Local\beamup\\` 59 | 60 | For languages that support building from source you can pass additional build 61 | options (like what is passed to `./configure` for Erlang) with either the 62 | environment variable `BEAMUP_BUILD_OPTIONS` or adding `default_build_options` to 63 | the configuration under the language section: 64 | 65 | ``` 66 | [erlang] 67 | default_build_options = "--enable-lock-counter" 68 | ``` 69 | 70 | Or: 71 | 72 | ``` 73 | BEAMUP_BUILD_OPTIONS="--enable-lock-counter" beamup build erlang -i latest-lock-counter latest 74 | ``` 75 | 76 | ### Install Languages 77 | 78 | The `build` command will compile a release and `install` will fetch a binary 79 | release. At this time Gleam and Elixir only the `install` command is supported`. 80 | 81 | Erlang installs are static builds from 82 | [gleam-community/erlang-linux-builds](https://github.com/gleam-community/erlang-linux-builds). 83 | To install a build that dynamically links against libc so that NIFs work use the 84 | argument `--static glibc` or `--static musl`. 85 | 86 | The string `latest` can be used instead of a release name to get the release 87 | marked latest in Github: 88 | 89 | ``` 90 | $ beamup install erlang latest 91 | ``` 92 | 93 | ``` 94 | $ beamup install gleam latest 95 | ``` 96 | 97 | ``` 98 | $ beamup install elixir latest 99 | ``` 100 | 101 | See the `releases ` sub-command to see available releases to 102 | build/install. 103 | 104 | ### Set Default Version 105 | 106 | Assuming you've built `OTP-25.3.2.7` you could set the default Erlang to use to 107 | it: 108 | 109 | ``` 110 | $ beamup default erlang OTP-25.3.2.7 111 | ``` 112 | 113 | ### Switch Version Used in Directory 114 | 115 | Using the `switch` sub-command either appends to or creates `./.beamup.toml` 116 | with an entry like `erlang = "OTP-25.3.2.7"` and running an Erlang command like 117 | `erl` in that directory will use that version instead of the global default. 118 | 119 | ### Other Commands 120 | 121 | - `releases `: List the available releases that can be installed 122 | - `update-links`: Update the hard links that exists for each language executable 123 | 124 | ### Install Components 125 | 126 | The `component install` command can install binary releases of tools, currently 127 | [The Erlang Language 128 | Platform](https://whatsapp.github.io/erlang-language-platform/) and 129 | [rebar3](https://rebar3.org/). 130 | 131 | The same as with a language you can specify a version of the component to use in 132 | the `.beamup.toml` file in a directory: 133 | 134 | ``` 135 | rebar3 = "3.23.0" 136 | ``` 137 | 138 | ## Differences with Erlup 139 | 140 | BEAMUp is the successor to [erlup](https://github.com/tsloughter/erlup) and has 141 | important differences. First, the configuration is TOML and not INI, see ` 142 | ~/.config/beamup/config.toml` and commands require specifying a language to work on, 143 | for example: 144 | 145 | ``` 146 | $ beamup install gleam v1.3.2 147 | ``` 148 | 149 | Another key difference is `build` will work on the tarball of Github releases by 150 | default, not clones of tags. Use `-b` (not supported yet) to install a tag or 151 | branch of a repository. 152 | 153 | 154 | 155 | ## Acknowledgments 156 | 157 | Inspiration for `erlup` is [erln8](https://github.com/metadave/erln8) by Dave 158 | Parfitt. He no longer maintains it and I figured I could use writing my own as a 159 | way to learn Rust. 160 | 161 | The switch to hardlinks instead of symlinks was taken from [rustup](https://rustup.rs/). 162 | -------------------------------------------------------------------------------- /src/github.rs: -------------------------------------------------------------------------------- 1 | use color_eyre::{eyre::eyre, eyre::Report, eyre::Result, eyre::WrapErr}; 2 | use console::{style, Emoji}; 3 | use indicatif::{HumanDuration, ProgressBar, ProgressStyle}; 4 | use std::fs::File; 5 | use std::path::Path; 6 | use std::path::PathBuf; 7 | use std::time::Duration; 8 | use std::time::Instant; 9 | 10 | // http://unicode.org/emoji/charts/full-emoji-list.html 11 | static CHECKMARK: Emoji = Emoji("✅", "✅ "); 12 | // static FAIL: Emoji = Emoji("❌", "❌ "); 13 | // static WARNING: Emoji = Emoji("🚫", "🚫"); 14 | 15 | pub type GithubRelease = String; 16 | 17 | pub struct GithubRepo { 18 | pub org: String, 19 | pub repo: String, 20 | } 21 | 22 | pub fn print_releases(GithubRepo { org, repo }: &GithubRepo) { 23 | let rt = setup_tokio(); 24 | 25 | let releases = rt.block_on(async { 26 | let octocrab = octocrab::instance(); 27 | octocrab.repos(org, repo).releases().list().send().await 28 | }); 29 | 30 | match releases { 31 | Ok(octocrab::Page { items, .. }) => { 32 | for release in items.iter() { 33 | let octocrab::models::repos::Release { tag_name, .. } = release; 34 | println!("{tag_name}"); 35 | } 36 | } 37 | _ => { 38 | error!("Failed to fetch releases for "); 39 | } 40 | } 41 | } 42 | 43 | pub fn download_release_tarball( 44 | out_dir: &Path, 45 | GithubRepo { org, repo }: &GithubRepo, 46 | tag: &String, 47 | ) -> Result { 48 | let rt = setup_tokio(); 49 | 50 | let release_result = if tag == "latest" { 51 | debug!("Getting latest release from {}/{}", org, repo); 52 | rt.block_on(async { 53 | octocrab::instance() 54 | .repos(org, repo) 55 | .releases() 56 | .get_latest() 57 | .await 58 | }) 59 | } else { 60 | debug!("Getting {} release from {}/{}", tag, org, repo); 61 | rt.block_on(async { 62 | octocrab::instance() 63 | .repos(org, repo) 64 | .releases() 65 | .get_by_tag(tag) 66 | .await 67 | }) 68 | }; 69 | 70 | let url = match release_result { 71 | Ok(octocrab::models::repos::Release { 72 | tarball_url: Some(tarball_url), 73 | .. 74 | }) => tarball_url, 75 | Ok(octocrab::models::repos::Release { 76 | tarball_url: None, .. 77 | }) => { 78 | let e: Report = eyre!("no source tarball found for release {tag}"); 79 | return Err(e); 80 | } 81 | Err(err) => { 82 | debug!("{err:?}"); 83 | return Err(err).wrap_err(format!( 84 | "Failed downloading release tarball Github release {tag:} from {org:}/{repo:}" 85 | )); 86 | } 87 | }; 88 | 89 | let file = out_dir.join(repo.to_owned() + ".tar.gz"); 90 | let dest = std::fs::File::create(&file) 91 | .wrap_err_with(|| format!("Failed to create asset download file {:?}", file))?; 92 | 93 | debug!( 94 | "Downloading release source tarball {:?} to {:?}", 95 | url.as_str(), 96 | file 97 | ); 98 | 99 | http_download( 100 | dest, 101 | url.as_str(), 102 | format!("Downloading release source tarball from {org}/{repo}"), 103 | )?; 104 | 105 | Ok(file) 106 | } 107 | 108 | pub fn download_asset( 109 | asset_prefix: ®ex::Regex, 110 | out_dir: &Path, 111 | GithubRepo { org, repo }: &GithubRepo, 112 | tag: &str, 113 | ) -> Result { 114 | let rt = setup_tokio(); 115 | 116 | let release_result = if tag == "latest" { 117 | debug!("Getting latest release from {}/{}", org, repo); 118 | rt.block_on(async { 119 | octocrab::instance() 120 | .repos(org, repo) 121 | .releases() 122 | .get_latest() 123 | .await 124 | }) 125 | } else { 126 | debug!("Getting {} release from {}/{}", tag, org, repo); 127 | rt.block_on(async { 128 | octocrab::instance() 129 | .repos(org, repo) 130 | .releases() 131 | .get_by_tag(tag) 132 | .await 133 | }) 134 | }; 135 | 136 | let assets = match release_result { 137 | Ok(octocrab::models::repos::Release { assets, .. }) => assets, 138 | Err(err) => { 139 | debug!("{err:?}"); 140 | return Err(err).wrap_err(format!( 141 | "Failed fetching Github release {tag:} from {org:}/{repo:}" 142 | )); 143 | } 144 | }; 145 | 146 | debug!("looking for asset {asset_prefix}"); 147 | match assets 148 | .iter() 149 | .find(|&asset| asset_prefix.is_match(&asset.name)) 150 | { 151 | Some(asset) => { 152 | let file = out_dir.join(&asset.name); 153 | let dest = std::fs::File::create(&file) 154 | .wrap_err_with(|| format!("Failed to create asset download file {:?}", file))?; 155 | 156 | debug!( 157 | "Downloading release asset {:?} to {:?}", 158 | &asset.browser_download_url, file 159 | ); 160 | 161 | http_download( 162 | dest, 163 | asset.browser_download_url.as_str(), 164 | format!("Downloading release source tarball from {org}/{repo}"), 165 | )?; 166 | 167 | Ok(file) 168 | } 169 | None => { 170 | let e: Report = 171 | eyre!("Release found but no asset on release matching prefix {asset_prefix} found"); 172 | 173 | Err(e).wrap_err("Github release asset download failed") 174 | } 175 | } 176 | } 177 | 178 | fn http_download(mut dest: File, url: &str, progress_msg: String) -> Result<()> { 179 | let started = Instant::now(); 180 | let response = ureq::get(url).call()?; 181 | 182 | if let Some(length) = response 183 | .header("content-length") 184 | .and_then(|l| l.parse().ok()) 185 | { 186 | let bar = indicatif::ProgressBar::new(!0) 187 | .with_prefix("Downloading") 188 | .with_style( 189 | indicatif::ProgressStyle::default_spinner() 190 | .template("{prefix:>12.bright.cyan} {spinner} {msg:.cyan}") 191 | .unwrap(), 192 | ) 193 | .with_message("preparing"); 194 | 195 | bar.set_style( 196 | indicatif::ProgressStyle::default_bar() 197 | .template("{prefix:>12.bright.cyan} [{bar:27}] {bytes:>9}/{total_bytes:9} {bytes_per_sec} {elapsed:>4}/{eta:4} - {msg:.cyan}")?.progress_chars("=> ")); 198 | bar.set_length(length); 199 | 200 | std::io::copy(&mut bar.wrap_read(response.into_reader()), &mut dest)?; 201 | 202 | bar.finish_and_clear(); 203 | } else { 204 | let spinner_style = ProgressStyle::default_spinner() 205 | .tick_chars("⠁⠂⠄⡀⢀⠠⠐⠈ ") 206 | .template("{prefix:.bold.dim} {spinner} {wide_msg}") 207 | .unwrap(); 208 | 209 | let pb = ProgressBar::new_spinner(); 210 | pb.set_style(spinner_style); 211 | pb.enable_steady_tick(Duration::from_millis(100)); 212 | 213 | pb.set_message(progress_msg.clone()); 214 | 215 | std::io::copy(&mut response.into_reader(), &mut dest)?; 216 | 217 | pb.println(format!(" {} {}", CHECKMARK, progress_msg)); 218 | 219 | pb.finish_and_clear(); 220 | 221 | println!( 222 | "{} download in {}", 223 | style("Finished").green().bold(), 224 | HumanDuration(started.elapsed()) 225 | ); 226 | } 227 | 228 | Ok(()) 229 | } 230 | 231 | // just need this for ocotocrab 232 | fn setup_tokio() -> tokio::runtime::Runtime { 233 | tokio::runtime::Builder::new_current_thread() 234 | .enable_all() 235 | .build() 236 | .unwrap() 237 | } 238 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | -------------------------------------------------------------------------------- /wix/main.wxs: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 46 | 47 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 69 | 70 | 80 | 81 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 106 | 111 | 112 | 113 | 114 | 122 | 123 | 124 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 145 | 146 | 150 | 151 | 152 | 153 | 154 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 191 | 1 192 | 1 193 | 194 | 195 | 196 | 197 | 202 | 203 | 204 | 205 | 213 | 214 | 215 | 216 | 224 | 225 | 226 | 227 | 228 | 229 | -------------------------------------------------------------------------------- /src/cmd/build.rs: -------------------------------------------------------------------------------- 1 | use crate::config; 2 | use crate::git::GitRef; 3 | use crate::github::download_release_tarball; 4 | use crate::languages; 5 | use color_eyre::{eyre::Result, eyre::WrapErr}; 6 | use console::Emoji; 7 | use flate2::read::GzDecoder; 8 | use indicatif::{HumanDuration, ProgressBar, ProgressStyle}; 9 | use std::env; 10 | use std::fs::File; 11 | use std::path::Path; 12 | use std::process::Command; 13 | use std::time::Duration; 14 | use std::time::Instant; 15 | use tar::Archive; 16 | use tempdir::TempDir; 17 | 18 | // http://unicode.org/emoji/charts/full-emoji-list.html 19 | static CHECKMARK: Emoji = Emoji("✅", "✅ "); 20 | static FAIL: Emoji = Emoji("❌", "❌ "); 21 | static WARNING: Emoji = Emoji("🚫", "🚫"); 22 | 23 | #[derive(Copy, Clone)] 24 | enum BuildResult { 25 | Success, 26 | Fail, 27 | } 28 | 29 | struct CheckContext<'a> { 30 | src_dir: &'a Path, 31 | install_dir: &'a Path, 32 | build_status: BuildResult, 33 | } 34 | 35 | enum CheckResult<'a> { 36 | Success, 37 | Warning(&'a str), 38 | Fail, 39 | } 40 | 41 | enum BuildStep<'a> { 42 | Exec(&'a str, Vec), 43 | Check(Box CheckResult<'a>>), 44 | } 45 | 46 | pub fn run( 47 | installable: &T, 48 | git_ref: &GitRef, 49 | id: &String, 50 | _repo: &Option, 51 | force: bool, 52 | config: &config::Config, 53 | ) -> Result { 54 | debug!("Building from source from git ref={git_ref} with id={id}"); 55 | 56 | let release_dir = &installable.release_dir(id)?; 57 | config::maybe_create_dir(release_dir, force)?; 58 | 59 | //maybe grab configure options from environment 60 | let key = "BEAMUP_BUILD_OPTIONS"; 61 | let user_build_options = match env::var(key) { 62 | Ok(options) => options, 63 | _ => installable.default_build_options(config), 64 | }; 65 | 66 | let github_repo = installable.source_repo(); 67 | let release = git_ref.to_string(); 68 | 69 | let out_dir = TempDir::new(github_repo.repo.as_str())?; 70 | let file = download_release_tarball(out_dir.path(), &github_repo, &release)?; 71 | 72 | let tar_gz = File::open(&file).wrap_err_with(|| { 73 | format!( 74 | "Downloaded Github release tarball {} into file {:?} not found", 75 | git_ref, &file 76 | ) 77 | })?; 78 | 79 | debug!("unpacking source tarball {tar_gz:?} to {out_dir:?}"); 80 | let tar = GzDecoder::new(tar_gz); 81 | let mut archive = Archive::new(tar); 82 | let unpack_dir = out_dir.path().join("unpack"); 83 | std::fs::create_dir_all(&unpack_dir)?; 84 | archive.unpack(&unpack_dir)?; 85 | 86 | let mut paths = std::fs::read_dir(&unpack_dir)?; 87 | let binding = paths.next().unwrap()?.path(); 88 | let unpacked_dir: &Path = binding.as_path(); 89 | std::fs::create_dir_all(&release_dir)?; 90 | build(&release_dir, unpacked_dir, user_build_options.as_str())?; 91 | 92 | Ok(release_dir.clone().into_os_string().into_string().unwrap()) 93 | } 94 | 95 | fn build(install_dir: &Path, dir: &Path, user_build_options0: &str) -> Result<()> { 96 | let num_cpus = num_cpus::get().to_string(); 97 | let spinner_style = ProgressStyle::default_spinner() 98 | .tick_chars("⠁⠂⠄⡀⢀⠠⠐⠈ ") 99 | .template("{prefix:.bold.dim} {spinner} {wide_msg}") 100 | .unwrap(); 101 | 102 | let pb = ProgressBar::new_spinner(); 103 | pb.set_style(spinner_style); 104 | pb.enable_steady_tick(Duration::from_millis(100)); 105 | 106 | // split the configure options into a vector of String in a shell sensitive way 107 | // eg. 108 | // from: 109 | // user_build_options0: --without-wx --without-observer --without-odbc --without-debugger --without-et --enable-builtin-zlib --without-javac CFLAGS="-g -O2 -march=native" 110 | // to: 111 | // user_build_options: ["--without-wx", "--without-observer", "--without-odbc", "--without-debugger", "--without-et", "--enable-builtin-zlib", "--without-javac", "CFLAGS=-g -O2 -march=native"] 112 | let mut user_build_options: Vec = shell_words::split(user_build_options0)?; 113 | // basic configure options must always include a prefix 114 | let mut build_options = vec![ 115 | "--prefix".to_string(), 116 | install_dir.to_str().unwrap().to_string(), 117 | ]; 118 | // append the user defined options 119 | build_options.append(&mut user_build_options); 120 | 121 | // declare the build pipeline steps 122 | let build_steps: [BuildStep; 6] = [ 123 | BuildStep::Exec("./configure", build_options), 124 | BuildStep::Check(Box::new(|context| { 125 | if has_openssl(context.src_dir) { 126 | CheckResult::Success 127 | } else { 128 | CheckResult::Warning("No usable OpenSSL found, please specify one with --with-ssl configure option, `crypto` application will not work in current build") 129 | } 130 | })), 131 | BuildStep::Exec("make", vec!["-j".to_string(), num_cpus.to_string()]), 132 | BuildStep::Exec( 133 | "make", 134 | vec![ 135 | "-j".to_string(), 136 | num_cpus.to_string(), 137 | "docs".to_string(), 138 | "DOC_TARGETS=chunks".to_string(), 139 | ], 140 | ), 141 | // after `make` we'll already know if this build failed or not, this allows us 142 | // to make a better decision in wether to delete the installation dir should there 143 | // be one. 144 | BuildStep::Check(Box::new(|context| { 145 | match context.build_status { 146 | BuildResult::Fail => { 147 | debug!("build has failed, aborting install to prevent overwriting a possibly working installation dir"); 148 | // this build has failed, we won't touch the previously existing install 149 | // dir, for all we know it could hold a previously working installation 150 | CheckResult::Fail 151 | } 152 | // if the build succeeded, then we check for an already existing 153 | // install dir, if we find one we can delete it and proceed to the 154 | // install phase 155 | BuildResult::Success => { 156 | // is install dir empty? courtesy of StackOverflow 157 | let is_empty = context 158 | .install_dir 159 | .read_dir() 160 | .map(|mut i| i.next().is_none()) 161 | .unwrap_or(false); 162 | if is_empty { 163 | // it's fine, it was probably us who created the dir just a moment ago, 164 | // that's why it's empty 165 | CheckResult::Success 166 | } else { 167 | debug!("found a non empty installation dir after a successful build, removing it"); 168 | // dir is not empty, maybe a working installation is already there, 169 | // delete the whole thing and proceed, we can go ahead with this 170 | // because we know we have a working build in our hands 171 | let _ = std::fs::remove_dir_all(context.install_dir); 172 | CheckResult::Success 173 | } 174 | } 175 | } 176 | })), 177 | BuildStep::Exec( 178 | "make", 179 | vec![ 180 | "-j".to_string(), 181 | num_cpus.to_string(), 182 | "install".to_string(), 183 | ], 184 | ), 185 | // needs to skip if no ex_doc found and give an info message 186 | // BuildStep::Exec( 187 | // "make", 188 | // vec![ 189 | // "-j".to_string(), 190 | // num_cpus.to_string(), 191 | // "install-docs".to_string(), 192 | // ], 193 | // ), 194 | ]; 195 | // execute them sequentially 196 | let mut build_status = BuildResult::Success; 197 | for step in build_steps.iter() { 198 | let step_started = Instant::now(); 199 | 200 | match step { 201 | BuildStep::Exec(command, args) => { 202 | // it only takes one exec command to fail for the build status 203 | // to be fail as well, a subsequent check build step can optionally decide 204 | // to fail the pipeline 205 | if let BuildResult::Fail = exec(command, args, dir, step_started, &pb) { 206 | build_status = BuildResult::Fail; 207 | } 208 | } 209 | BuildStep::Check(fun) => { 210 | let context = CheckContext { 211 | src_dir: dir, 212 | install_dir, 213 | build_status, 214 | }; 215 | match fun(&context) { 216 | CheckResult::Success => { 217 | debug!("success"); 218 | } 219 | CheckResult::Warning(warning) => { 220 | debug!("{}", warning); 221 | pb.set_message(warning); 222 | pb.println(format!(" {} {}", WARNING, warning)); 223 | } 224 | CheckResult::Fail => { 225 | // abort 226 | pb.finish_and_clear(); 227 | std::process::exit(1); 228 | } 229 | } 230 | } 231 | } 232 | } 233 | 234 | Ok(()) 235 | } 236 | 237 | fn has_openssl(src_dir: &Path) -> bool { 238 | // check that lib/crypto/SKIP doesn't exist, 239 | // if it does it means something went wrong with OpenSSL 240 | !src_dir.join("./lib/crypto/SKIP").exists() 241 | } 242 | 243 | fn exec( 244 | command: &str, 245 | args: &Vec, 246 | dir: &Path, 247 | started_ts: Instant, 248 | pb: &ProgressBar, 249 | ) -> BuildResult { 250 | debug!("Running {} {:?}", command, args); 251 | pb.set_message(format!("{} {}", command, args.join(" "))); 252 | match Command::new(command).args(args).current_dir(dir).output() { 253 | Err(e) => { 254 | pb.println(format!(" {} {} {}", FAIL, command, args.join(" "))); 255 | error!("build failed: {}", e); 256 | BuildResult::Fail 257 | } 258 | Ok(output) => { 259 | debug!("stdout: {}", String::from_utf8_lossy(&output.stdout)); 260 | debug!("stderr: {}", String::from_utf8_lossy(&output.stderr)); 261 | 262 | match output.status.success() { 263 | true => { 264 | pb.println(format!( 265 | " {} {} {} (done in {})", 266 | CHECKMARK, 267 | command, 268 | args.join(" "), 269 | HumanDuration(started_ts.elapsed()) 270 | )); 271 | BuildResult::Success 272 | } 273 | false => { 274 | error!("stdout: {}", String::from_utf8_lossy(&output.stdout)); 275 | pb.println(format!(" {} {} {}", FAIL, command, args.join(" "))); 276 | BuildResult::Fail 277 | } 278 | } 279 | } 280 | } 281 | } 282 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | # This file was autogenerated by dist: https://axodotdev.github.io/cargo-dist 2 | # 3 | # Copyright 2022-2024, axodotdev 4 | # SPDX-License-Identifier: MIT or Apache-2.0 5 | # 6 | # CI that: 7 | # 8 | # * checks for a Git Tag that looks like a release 9 | # * builds artifacts with dist (archives, installers, hashes) 10 | # * uploads those artifacts to temporary workflow zip 11 | # * on success, uploads the artifacts to a GitHub Release 12 | # 13 | # Note that the GitHub Release will be created with a generated 14 | # title/body based on your changelogs. 15 | 16 | name: Release 17 | permissions: 18 | "contents": "write" 19 | 20 | # This task will run whenever you push a git tag that looks like a version 21 | # like "1.0.0", "v0.1.0-prerelease.1", "my-app/0.1.0", "releases/v1.0.0", etc. 22 | # Various formats will be parsed into a VERSION and an optional PACKAGE_NAME, where 23 | # PACKAGE_NAME must be the name of a Cargo package in your workspace, and VERSION 24 | # must be a Cargo-style SemVer Version (must have at least major.minor.patch). 25 | # 26 | # If PACKAGE_NAME is specified, then the announcement will be for that 27 | # package (erroring out if it doesn't have the given version or isn't dist-able). 28 | # 29 | # If PACKAGE_NAME isn't specified, then the announcement will be for all 30 | # (dist-able) packages in the workspace with that version (this mode is 31 | # intended for workspaces with only one dist-able package, or with all dist-able 32 | # packages versioned/released in lockstep). 33 | # 34 | # If you push multiple tags at once, separate instances of this workflow will 35 | # spin up, creating an independent announcement for each one. However, GitHub 36 | # will hard limit this to 3 tags per commit, as it will assume more tags is a 37 | # mistake. 38 | # 39 | # If there's a prerelease-style suffix to the version, then the release(s) 40 | # will be marked as a prerelease. 41 | on: 42 | pull_request: 43 | push: 44 | tags: 45 | - '**[0-9]+.[0-9]+.[0-9]+*' 46 | 47 | jobs: 48 | # Run 'dist plan' (or host) to determine what tasks we need to do 49 | plan: 50 | runs-on: "ubuntu-22.04" 51 | outputs: 52 | val: ${{ steps.plan.outputs.manifest }} 53 | tag: ${{ !github.event.pull_request && github.ref_name || '' }} 54 | tag-flag: ${{ !github.event.pull_request && format('--tag={0}', github.ref_name) || '' }} 55 | publishing: ${{ !github.event.pull_request }} 56 | env: 57 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 58 | steps: 59 | - uses: actions/checkout@v4 60 | with: 61 | persist-credentials: false 62 | submodules: recursive 63 | - name: Install dist 64 | # we specify bash to get pipefail; it guards against the `curl` command 65 | # failing. otherwise `sh` won't catch that `curl` returned non-0 66 | shell: bash 67 | run: "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/v0.30.2/cargo-dist-installer.sh | sh" 68 | - name: Cache dist 69 | uses: actions/upload-artifact@v4 70 | with: 71 | name: cargo-dist-cache 72 | path: ~/.cargo/bin/dist 73 | # sure would be cool if github gave us proper conditionals... 74 | # so here's a doubly-nested ternary-via-truthiness to try to provide the best possible 75 | # functionality based on whether this is a pull_request, and whether it's from a fork. 76 | # (PRs run on the *source* but secrets are usually on the *target* -- that's *good* 77 | # but also really annoying to build CI around when it needs secrets to work right.) 78 | - id: plan 79 | run: | 80 | dist ${{ (!github.event.pull_request && format('host --steps=create --tag={0}', github.ref_name)) || 'plan' }} --output-format=json > plan-dist-manifest.json 81 | echo "dist ran successfully" 82 | cat plan-dist-manifest.json 83 | echo "manifest=$(jq -c "." plan-dist-manifest.json)" >> "$GITHUB_OUTPUT" 84 | - name: "Upload dist-manifest.json" 85 | uses: actions/upload-artifact@v4 86 | with: 87 | name: artifacts-plan-dist-manifest 88 | path: plan-dist-manifest.json 89 | 90 | # Build and packages all the platform-specific things 91 | build-local-artifacts: 92 | name: build-local-artifacts (${{ join(matrix.targets, ', ') }}) 93 | # Let the initial task tell us to not run (currently very blunt) 94 | needs: 95 | - plan 96 | if: ${{ fromJson(needs.plan.outputs.val).ci.github.artifacts_matrix.include != null && (needs.plan.outputs.publishing == 'true' || fromJson(needs.plan.outputs.val).ci.github.pr_run_mode == 'upload') }} 97 | strategy: 98 | fail-fast: false 99 | # Target platforms/runners are computed by dist in create-release. 100 | # Each member of the matrix has the following arguments: 101 | # 102 | # - runner: the github runner 103 | # - dist-args: cli flags to pass to dist 104 | # - install-dist: expression to run to install dist on the runner 105 | # 106 | # Typically there will be: 107 | # - 1 "global" task that builds universal installers 108 | # - N "local" tasks that build each platform's binaries and platform-specific installers 109 | matrix: ${{ fromJson(needs.plan.outputs.val).ci.github.artifacts_matrix }} 110 | runs-on: ${{ matrix.runner }} 111 | container: ${{ matrix.container && matrix.container.image || null }} 112 | env: 113 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 114 | BUILD_MANIFEST_NAME: target/distrib/${{ join(matrix.targets, '-') }}-dist-manifest.json 115 | steps: 116 | - name: enable windows longpaths 117 | run: | 118 | git config --global core.longpaths true 119 | - uses: actions/checkout@v4 120 | with: 121 | persist-credentials: false 122 | submodules: recursive 123 | - name: Install Rust non-interactively if not already installed 124 | if: ${{ matrix.container }} 125 | run: | 126 | if ! command -v cargo > /dev/null 2>&1; then 127 | curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y 128 | echo "$HOME/.cargo/bin" >> $GITHUB_PATH 129 | fi 130 | - name: Install dist 131 | run: ${{ matrix.install_dist.run }} 132 | # Get the dist-manifest 133 | - name: Fetch local artifacts 134 | uses: actions/download-artifact@v4 135 | with: 136 | pattern: artifacts-* 137 | path: target/distrib/ 138 | merge-multiple: true 139 | - name: Install dependencies 140 | run: | 141 | ${{ matrix.packages_install }} 142 | - name: Build artifacts 143 | run: | 144 | # Actually do builds and make zips and whatnot 145 | dist build ${{ needs.plan.outputs.tag-flag }} --print=linkage --output-format=json ${{ matrix.dist_args }} > dist-manifest.json 146 | echo "dist ran successfully" 147 | - id: cargo-dist 148 | name: Post-build 149 | # We force bash here just because github makes it really hard to get values up 150 | # to "real" actions without writing to env-vars, and writing to env-vars has 151 | # inconsistent syntax between shell and powershell. 152 | shell: bash 153 | run: | 154 | # Parse out what we just built and upload it to scratch storage 155 | echo "paths<> "$GITHUB_OUTPUT" 156 | dist print-upload-files-from-manifest --manifest dist-manifest.json >> "$GITHUB_OUTPUT" 157 | echo "EOF" >> "$GITHUB_OUTPUT" 158 | 159 | cp dist-manifest.json "$BUILD_MANIFEST_NAME" 160 | - name: "Upload artifacts" 161 | uses: actions/upload-artifact@v4 162 | with: 163 | name: artifacts-build-local-${{ join(matrix.targets, '_') }} 164 | path: | 165 | ${{ steps.cargo-dist.outputs.paths }} 166 | ${{ env.BUILD_MANIFEST_NAME }} 167 | 168 | # Build and package all the platform-agnostic(ish) things 169 | build-global-artifacts: 170 | needs: 171 | - plan 172 | - build-local-artifacts 173 | runs-on: "ubuntu-22.04" 174 | env: 175 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 176 | BUILD_MANIFEST_NAME: target/distrib/global-dist-manifest.json 177 | steps: 178 | - uses: actions/checkout@v4 179 | with: 180 | persist-credentials: false 181 | submodules: recursive 182 | - name: Install cached dist 183 | uses: actions/download-artifact@v4 184 | with: 185 | name: cargo-dist-cache 186 | path: ~/.cargo/bin/ 187 | - run: chmod +x ~/.cargo/bin/dist 188 | # Get all the local artifacts for the global tasks to use (for e.g. checksums) 189 | - name: Fetch local artifacts 190 | uses: actions/download-artifact@v4 191 | with: 192 | pattern: artifacts-* 193 | path: target/distrib/ 194 | merge-multiple: true 195 | - id: cargo-dist 196 | shell: bash 197 | run: | 198 | dist build ${{ needs.plan.outputs.tag-flag }} --output-format=json "--artifacts=global" > dist-manifest.json 199 | echo "dist ran successfully" 200 | 201 | # Parse out what we just built and upload it to scratch storage 202 | echo "paths<> "$GITHUB_OUTPUT" 203 | jq --raw-output ".upload_files[]" dist-manifest.json >> "$GITHUB_OUTPUT" 204 | echo "EOF" >> "$GITHUB_OUTPUT" 205 | 206 | cp dist-manifest.json "$BUILD_MANIFEST_NAME" 207 | - name: "Upload artifacts" 208 | uses: actions/upload-artifact@v4 209 | with: 210 | name: artifacts-build-global 211 | path: | 212 | ${{ steps.cargo-dist.outputs.paths }} 213 | ${{ env.BUILD_MANIFEST_NAME }} 214 | # Determines if we should publish/announce 215 | host: 216 | needs: 217 | - plan 218 | - build-local-artifacts 219 | - build-global-artifacts 220 | # Only run if we're "publishing", and only if plan, local and global didn't fail (skipped is fine) 221 | if: ${{ always() && needs.plan.result == 'success' && needs.plan.outputs.publishing == 'true' && (needs.build-global-artifacts.result == 'skipped' || needs.build-global-artifacts.result == 'success') && (needs.build-local-artifacts.result == 'skipped' || needs.build-local-artifacts.result == 'success') }} 222 | env: 223 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 224 | runs-on: "ubuntu-22.04" 225 | outputs: 226 | val: ${{ steps.host.outputs.manifest }} 227 | steps: 228 | - uses: actions/checkout@v4 229 | with: 230 | persist-credentials: false 231 | submodules: recursive 232 | - name: Install cached dist 233 | uses: actions/download-artifact@v4 234 | with: 235 | name: cargo-dist-cache 236 | path: ~/.cargo/bin/ 237 | - run: chmod +x ~/.cargo/bin/dist 238 | # Fetch artifacts from scratch-storage 239 | - name: Fetch artifacts 240 | uses: actions/download-artifact@v4 241 | with: 242 | pattern: artifacts-* 243 | path: target/distrib/ 244 | merge-multiple: true 245 | - id: host 246 | shell: bash 247 | run: | 248 | dist host ${{ needs.plan.outputs.tag-flag }} --steps=upload --steps=release --output-format=json > dist-manifest.json 249 | echo "artifacts uploaded and released successfully" 250 | cat dist-manifest.json 251 | echo "manifest=$(jq -c "." dist-manifest.json)" >> "$GITHUB_OUTPUT" 252 | - name: "Upload dist-manifest.json" 253 | uses: actions/upload-artifact@v4 254 | with: 255 | # Overwrite the previous copy 256 | name: artifacts-dist-manifest 257 | path: dist-manifest.json 258 | # Create a GitHub Release while uploading all files to it 259 | - name: "Download GitHub Artifacts" 260 | uses: actions/download-artifact@v4 261 | with: 262 | pattern: artifacts-* 263 | path: artifacts 264 | merge-multiple: true 265 | - name: Cleanup 266 | run: | 267 | # Remove the granular manifests 268 | rm -f artifacts/*-dist-manifest.json 269 | - name: Create GitHub Release 270 | env: 271 | PRERELEASE_FLAG: "${{ fromJson(steps.host.outputs.manifest).announcement_is_prerelease && '--prerelease' || '' }}" 272 | ANNOUNCEMENT_TITLE: "${{ fromJson(steps.host.outputs.manifest).announcement_title }}" 273 | ANNOUNCEMENT_BODY: "${{ fromJson(steps.host.outputs.manifest).announcement_github_body }}" 274 | RELEASE_COMMIT: "${{ github.sha }}" 275 | run: | 276 | # Write and read notes from a file to avoid quoting breaking things 277 | echo "$ANNOUNCEMENT_BODY" > $RUNNER_TEMP/notes.txt 278 | 279 | gh release create "${{ needs.plan.outputs.tag }}" --target "$RELEASE_COMMIT" $PRERELEASE_FLAG --title "$ANNOUNCEMENT_TITLE" --notes-file "$RUNNER_TEMP/notes.txt" artifacts/* 280 | 281 | announce: 282 | needs: 283 | - plan 284 | - host 285 | # use "always() && ..." to allow us to wait for all publish jobs while 286 | # still allowing individual publish jobs to skip themselves (for prereleases). 287 | # "host" however must run to completion, no skipping allowed! 288 | if: ${{ always() && needs.host.result == 'success' }} 289 | runs-on: "ubuntu-22.04" 290 | env: 291 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 292 | steps: 293 | - uses: actions/checkout@v4 294 | with: 295 | persist-credentials: false 296 | submodules: recursive 297 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | extern crate clap; 2 | 3 | #[macro_use] 4 | extern crate log; 5 | #[macro_use] 6 | extern crate strum; 7 | 8 | use clap::{Args, Command, CommandFactory, Parser, Subcommand}; 9 | use clap_complete::{generate, Generator, Shell}; 10 | use console::style; 11 | use log::{Level, LevelFilter, Record}; 12 | use std::env; 13 | use std::io::Write; 14 | use std::path::Path; 15 | use std::path::PathBuf; 16 | use std::process; 17 | 18 | mod config; 19 | 20 | use color_eyre::{config::HookBuilder, eyre::eyre, eyre::Report, eyre::Result}; 21 | 22 | mod cmd; 23 | mod components; 24 | mod git; 25 | mod github; 26 | mod languages; 27 | mod links; 28 | mod run; 29 | mod utils; 30 | 31 | #[derive(Parser, Debug)] 32 | #[command(version, about = "Manage BEAM language installs.", long_about = None)] 33 | #[command(propagate_version = true)] 34 | struct Cli { 35 | #[arg(short, long)] 36 | config: Option, 37 | 38 | #[command(subcommand)] 39 | subcommand: SubCommands, 40 | } 41 | 42 | #[derive(Subcommand, Debug)] 43 | enum SubCommands { 44 | Generate(GenerateArgs), 45 | 46 | /// List supported languages 47 | Languages, 48 | 49 | /// List supported components 50 | Components, 51 | 52 | /// Update binary symlinks to erlup executable 53 | UpdateLinks, 54 | 55 | /// List installed languages 56 | List, 57 | 58 | /// Fetch available releases for language 59 | Releases(ReleasesArgs), 60 | 61 | /// Fetch latest tags for repo 62 | Fetch(RepoArgs), 63 | 64 | /// List available tags to build for a repo 65 | Tags(RepoArgs), 66 | 67 | /// List available branches to build for a repo 68 | Branches(RepoArgs), 69 | 70 | /// Switch install to use by id 71 | Switch(IdArgs), 72 | 73 | /// Set default install to use by id 74 | Default(IdArgs), 75 | 76 | /// Deletes an install by id 77 | Delete(IdArgs), 78 | 79 | /// Build and install by branch of tag name 80 | Build(BuildArgs), 81 | 82 | /// Install binary release of language 83 | Install(InstallArgs), 84 | 85 | /// Manage components 86 | Component(ComponentSubCommands), 87 | 88 | /// Update repos to the config 89 | Repo(RepoSubCommands), 90 | 91 | /// Add or remove a link to an existing install 92 | Link(LinkSubCommands), 93 | } 94 | 95 | #[derive(Args, Debug)] 96 | struct GenerateArgs { 97 | // Shell to generate completions for 98 | shell: Shell, 99 | } 100 | 101 | #[derive(Args, Debug)] 102 | struct ReleasesArgs { 103 | /// Language to list releases for 104 | language: languages::Language, 105 | 106 | /// Which repo to use for command 107 | #[arg(short, long)] 108 | repo: Option, 109 | } 110 | 111 | #[derive(Args, Debug)] 112 | struct RepoArgs { 113 | /// Which repo to use for command 114 | #[arg(short, long)] 115 | repo: Option, 116 | } 117 | 118 | #[derive(Args, Debug)] 119 | struct IdArgs { 120 | /// Language to use 121 | language: languages::Language, 122 | 123 | /// Id of the install 124 | id: String, 125 | } 126 | 127 | #[derive(Args, Debug)] 128 | struct BuildArgs { 129 | /// Language to build a release or branch of 130 | language: languages::Language, 131 | 132 | /// Release to build 133 | release: Option, 134 | 135 | /// Branch or tag of the repo 136 | #[arg(short, long)] 137 | branch: Option, 138 | 139 | /// Id to give the build 140 | #[arg(short, long)] 141 | id: Option, 142 | 143 | /// Which repo to use for command 144 | #[arg(short, long)] 145 | repo: Option, 146 | 147 | /// Forces a build disregarding any previously existing ones 148 | #[arg(short, long)] 149 | force: bool, 150 | } 151 | 152 | #[derive(Args, Debug)] 153 | struct InstallArgs { 154 | /// Language to install release of 155 | language: languages::Language, 156 | 157 | /// Release version to install 158 | release: String, 159 | 160 | /// Id to give the install 161 | #[arg(short, long)] 162 | id: Option, 163 | 164 | /// Where to grab the releases 165 | #[arg(short, long)] 166 | repo: Option, 167 | 168 | /// Forces an install disregarding any previously existing ones 169 | #[arg(short, long)] 170 | force: bool, 171 | 172 | /// For Erlang only. Select the libc the install wil be built to dynamically link against. 173 | #[arg(short, long)] 174 | libc: Option, 175 | } 176 | 177 | #[derive(Args, Debug)] 178 | struct ComponentSubCommands { 179 | #[command(subcommand)] 180 | cmd: ComponentCmds, 181 | } 182 | 183 | #[derive(Subcommand, Debug)] 184 | enum ComponentCmds { 185 | /// Install a component 186 | Install(ComponentInstallArgs), 187 | } 188 | 189 | #[derive(Args, Debug)] 190 | struct ComponentInstallArgs { 191 | /// Component to install 192 | component: components::Kind, 193 | 194 | /// Release version to install 195 | release: String, 196 | 197 | /// Id to give the install 198 | #[arg(short, long)] 199 | id: Option, 200 | 201 | /// Where to grab the releases 202 | #[arg(short, long)] 203 | repo: Option, 204 | 205 | /// Forces an install disregarding any previously existing ones 206 | #[arg(short, long)] 207 | force: bool, 208 | } 209 | 210 | #[derive(Args, Debug)] 211 | struct RepoSubCommands { 212 | #[command(subcommand)] 213 | cmd: RepoCmds, 214 | } 215 | 216 | #[derive(Subcommand, Debug)] 217 | enum RepoCmds { 218 | /// Add repo to the configuration 219 | Add(RepoAddArgs), 220 | 221 | /// Remove repo from the configuration 222 | Rm(RepoRmArgs), 223 | 224 | /// List available repos 225 | Ls, 226 | } 227 | 228 | #[derive(Args, Debug)] 229 | struct RepoAddArgs { 230 | /// Language to add repo for 231 | language: languages::Language, 232 | 233 | /// id of the repo to add 234 | id: String, 235 | 236 | /// Url of the git repo for the repo 237 | repo: String, 238 | } 239 | 240 | #[derive(Args, Debug)] 241 | struct RepoRmArgs { 242 | /// Language to remove repo for 243 | language: languages::Language, 244 | 245 | /// id of the repo to remove 246 | id: String, 247 | } 248 | 249 | #[derive(Args, Debug)] 250 | struct LinkSubCommands { 251 | #[command(subcommand)] 252 | cmd: LinkCmds, 253 | } 254 | 255 | #[derive(Subcommand, Debug)] 256 | enum LinkCmds { 257 | /// Add link to existing installation of language 258 | Add(LinkAddArgs), 259 | 260 | /// Remove link to installation language 261 | Rm(LinkRmArgs), 262 | } 263 | 264 | #[derive(Args, Debug)] 265 | struct LinkAddArgs { 266 | /// Language to add existing installation for 267 | language: languages::Language, 268 | 269 | /// id of the installation to link to 270 | id: String, 271 | 272 | /// Path of the existing installation 273 | path: String, 274 | } 275 | 276 | #[derive(Args, Debug)] 277 | struct LinkRmArgs { 278 | /// Language to remove existing installation for 279 | language: languages::Language, 280 | 281 | /// id of the installation to remove 282 | id: String, 283 | } 284 | 285 | fn print_completions(gen: G, cmd: &mut Command) { 286 | generate(gen, cmd, cmd.get_name().to_string(), &mut std::io::stdout()); 287 | } 288 | 289 | fn handle_command(_bin_path: PathBuf) -> Result<(), Report> { 290 | let cli = Cli::parse(); 291 | 292 | let (config_file, config) = match &cli.config { 293 | Some(file) => (file.to_owned(), config::read_config(file.to_owned())), 294 | None => config::home_config()?, 295 | }; 296 | 297 | match &cli.subcommand { 298 | SubCommands::Generate(GenerateArgs { shell }) => { 299 | debug!("Generating completion file for {shell:?}..."); 300 | print_completions(*shell, &mut Cli::command()); 301 | Ok(()) 302 | } 303 | SubCommands::Languages => { 304 | debug!("running list"); 305 | println!("Languages:\n"); 306 | languages::print(); 307 | Ok(()) 308 | } 309 | SubCommands::Components => { 310 | debug!("running list"); 311 | println!("Components:\n"); 312 | components::print(); 313 | Ok(()) 314 | } 315 | SubCommands::List => { 316 | debug!("running list"); 317 | cmd::list::run(&config); 318 | Ok(()) 319 | } 320 | SubCommands::Releases(ReleasesArgs { language, .. }) => { 321 | debug!("running releases: language={:?}", language); 322 | 323 | // TODO: should return Result type 324 | cmd::releases::run(language); 325 | Ok(()) 326 | } 327 | SubCommands::Install(InstallArgs { 328 | language, 329 | release, 330 | id, 331 | repo, 332 | force, 333 | libc, 334 | }) => { 335 | debug!( 336 | "running install: {:?} {} {:?} {:?} {:?} {:?}", 337 | language, release, id, repo, force, libc 338 | ); 339 | 340 | check_if_install_supported(language)?; 341 | 342 | // if no user supplied id then use the name of 343 | // the release to install 344 | let id = id.as_ref().unwrap_or(release); 345 | 346 | info!( 347 | "Downloading and installing {:?} for release={} id={}", 348 | language, release, id 349 | ); 350 | 351 | let dir = cmd::install::run(language, id, release, libc, *force)?; 352 | cmd::update_links::run(Some(language), &config)?; 353 | 354 | config::add_install(language, id, release, dir, config_file, config)?; 355 | 356 | info!( 357 | "Completed install of {:?} for release={} id={}", 358 | language, release, id 359 | ); 360 | 361 | Ok(()) 362 | } 363 | SubCommands::UpdateLinks => { 364 | debug!("running update-links"); 365 | 366 | cmd::update_links::run(None, &config)?; 367 | 368 | info!("Updated links of language binaries to current beamup install"); 369 | 370 | Ok(()) 371 | } 372 | SubCommands::Default(IdArgs { language, id }) => { 373 | debug!("running default: {:?} {:?}", language, id); 374 | 375 | info!( 376 | "Setting default {:?} to use to install of id{}", 377 | language, id 378 | ); 379 | 380 | cmd::default::run(language, id, config_file, config) 381 | } 382 | SubCommands::Switch(IdArgs { language, id }) => { 383 | debug!("running switch: {:?} {:?}", language, id); 384 | 385 | info!("Switching local {:?} to use install of id={}", language, id); 386 | 387 | cmd::switch::run(language, id, config) 388 | } 389 | SubCommands::Build(BuildArgs { 390 | language, 391 | release, 392 | branch, 393 | id, 394 | repo, 395 | force, 396 | }) => { 397 | debug!( 398 | "running build: {:?} {:?} {:?} {:?} {:?} {:?}", 399 | language, release, branch, id, repo, force 400 | ); 401 | 402 | if *language != languages::Language::Erlang { 403 | return Err(eyre!( 404 | "build command not supported yet for language {language:?}" 405 | )); 406 | } 407 | 408 | if std::env::consts::OS == "windows" { 409 | return Err(eyre!("build command not supported yet for Windows")); 410 | } 411 | 412 | let git_ref = match release { 413 | None => match branch { 414 | None => { 415 | return Err(eyre!( 416 | "build command needs a release argument or the -b option" 417 | )) 418 | } 419 | Some(branch) => git::GitRef::Branch(branch.to_owned()), 420 | }, 421 | Some(release) => git::GitRef::Release(release.to_owned()), 422 | }; 423 | let id = id.clone().unwrap_or(git_ref.to_string()); 424 | 425 | info!("Building {:?} for ref={} id={}", language, git_ref, id); 426 | let dir = cmd::build::run(language, &git_ref, &id, repo, *force, &config)?; 427 | 428 | cmd::update_links::run(Some(language), &config)?; 429 | 430 | config::add_install( 431 | language, 432 | &id, 433 | &git_ref.to_string(), 434 | dir, 435 | config_file, 436 | config, 437 | )?; 438 | 439 | info!( 440 | "Completed build and install of {:?} for ref={} id={}", 441 | language, git_ref, id 442 | ); 443 | 444 | Ok(()) 445 | } 446 | SubCommands::Component(ComponentSubCommands { 447 | cmd: 448 | ComponentCmds::Install(ComponentInstallArgs { 449 | component, 450 | release, 451 | id, 452 | repo: _repo, 453 | force, 454 | }), 455 | }) => { 456 | debug!("running component install {component:?}"); 457 | 458 | check_if_component_install_supported()?; 459 | 460 | // if no user supplied id then use the name of 461 | // the release to install 462 | let id = id.as_ref().unwrap_or(release); 463 | 464 | let c = components::Component::new(component.clone(), release)?; 465 | 466 | let release_dir = cmd::component_install::run(&c, release, *force)?; 467 | 468 | let bin_dir = config::bin_dir(); 469 | let _ = std::fs::create_dir_all(&bin_dir); 470 | 471 | let (bins, _): (Vec, Vec) = c.bins.into_iter().unzip(); 472 | 473 | links::update(bins.into_iter(), &bin_dir)?; 474 | 475 | config::add_component_install( 476 | component, 477 | id, 478 | &release.to_string(), 479 | release_dir.to_string(), 480 | config_file, 481 | config, 482 | )?; 483 | 484 | info!("Completed install of component {component:?} with id={id}"); 485 | 486 | Ok(()) 487 | } 488 | 489 | _ => Err(eyre!("subcommand not implemented yet")), 490 | } 491 | } 492 | 493 | // only Elixir and Gleam support install on any platform 494 | // Erlang only on Windows 495 | fn check_if_install_supported(language: &languages::Language) -> Result<()> { 496 | if *language == languages::Language::Erlang { 497 | // install command for Erlang only supports Windows on x86 or x86_64 at this time 498 | match (std::env::consts::ARCH, std::env::consts::OS) { 499 | ("x86", "windows") => return Ok(()), 500 | ("x86_64", "windows") => return Ok(()), 501 | ("x86_64", "macos") => return Ok(()), 502 | ("aarch64", "macos") => return Ok(()), 503 | ("x86_64", "linux") => return Ok(()), 504 | ("aarch64", "linux") => return Ok(()), 505 | (os, arch) => { 506 | return Err(eyre!( 507 | "install command not supported yet for language {language:?} on {os:?} {arch:?}" 508 | )) 509 | } 510 | } 511 | } 512 | 513 | if *language == languages::Language::Elixir { 514 | // catch when no Erlang is installed and made the default 515 | match config::get_otp_major_vsn() { 516 | Err(_) => return Err(eyre!("No default Erlang installation found. Install an Erlang version, like `beamup install erlang latest` or set a default with `beamup default erlang ` first.")), 517 | _ => () 518 | } 519 | } 520 | 521 | Ok(()) 522 | } 523 | 524 | // ELP and rebar3 don't (yet) provide Windows binaries 525 | fn check_if_component_install_supported() -> Result<()> { 526 | match (std::env::consts::ARCH, std::env::consts::OS) { 527 | (_, "windows") => Err(eyre!( 528 | "component install command not supported yet on Windows" 529 | )), 530 | _ => Ok(()), 531 | } 532 | } 533 | 534 | fn setup_logging() { 535 | let format = |buf: &mut env_logger::fmt::Formatter, record: &Record| { 536 | if record.level() == Level::Error { 537 | writeln!(buf, "{}", style(format!("{}", record.args())).red()) 538 | } else if record.level() == Level::Info { 539 | writeln!(buf, "{}", record.args()) 540 | } else { 541 | writeln!(buf, "{}", style(format!("{}", record.args())).blue()) 542 | } 543 | }; 544 | 545 | let key = "DEBUG"; 546 | let level = match env::var(key) { 547 | Ok(_) => LevelFilter::Debug, 548 | _ => LevelFilter::Info, 549 | }; 550 | 551 | env_logger::builder() 552 | .format(format) 553 | .filter(None, level) 554 | .init(); 555 | } 556 | 557 | fn main() -> Result<(), Report> { 558 | // color_eyre::install()?; 559 | setup_logging(); 560 | 561 | let mut args = env::args(); 562 | let binname = args.next().unwrap(); 563 | let f = Path::new(&binname).file_name().unwrap(); 564 | 565 | HookBuilder::default() 566 | .display_location_section(false) 567 | .install()?; 568 | 569 | if f.eq("beamup") || f.eq("beamup.exe") { 570 | match env::current_exe() { 571 | Ok(bin_path) => { 572 | debug!("current bin path: {}", bin_path.display()); 573 | handle_command(bin_path) 574 | } 575 | Err(e) => { 576 | println!("failed to get current bin path: {}", e); 577 | process::exit(1) 578 | } 579 | } 580 | } else { 581 | let (_, config) = config::home_config()?; 582 | match languages::bins(&config) 583 | .iter() 584 | .find(|&(k, _)| *k == f.to_str().unwrap()) 585 | { 586 | Some((c, _)) => { 587 | let bin = Path::new(c).file_name().unwrap(); 588 | run::run(bin.to_str().unwrap(), args) 589 | } 590 | None => match components::bins().iter().find(|(e, _)| e.as_str() == f) { 591 | Some((e, k)) => run::run_component(e, k, args), 592 | None => Err(eyre!("beamup found no such command: {f:?}")), 593 | }, 594 | } 595 | } 596 | } 597 | -------------------------------------------------------------------------------- /src/config.rs: -------------------------------------------------------------------------------- 1 | use crate::components; 2 | use crate::languages; 3 | use color_eyre::{eyre::eyre, eyre::Report, eyre::Result}; 4 | use serde::{Deserialize, Serialize}; 5 | use std::fs; 6 | use std::io::Write; 7 | use std::path::*; 8 | 9 | static LOCAL_CONFIG_FILE: &str = ".beamup.toml"; 10 | static CONFIG_FILE: &str = "config.toml"; 11 | 12 | #[derive(Debug, Deserialize, Serialize, Clone)] 13 | pub struct Config { 14 | install_dir: String, 15 | erlang: Option, 16 | gleam: Option, 17 | elixir: Option, 18 | elp: Option, 19 | rebar3: Option, 20 | } 21 | 22 | #[derive(Debug, Deserialize, Serialize, Clone, Default)] 23 | pub struct LanguageConfig { 24 | default: Option, 25 | default_build_options: Option, 26 | installs: toml::Table, 27 | } 28 | 29 | #[derive(Debug, Deserialize, Serialize, Clone, Default)] 30 | pub struct ComponentConfig { 31 | default: Option, 32 | default_build_options: Option, 33 | installs: toml::Table, 34 | } 35 | 36 | pub fn print_ids(config: &Config) { 37 | println!("Elixir:"); 38 | config.elixir.as_ref().map(print_language_ids); 39 | println!(); 40 | println!("Erlang:"); 41 | config.erlang.as_ref().map(print_language_ids); 42 | println!(); 43 | println!("Gleam:"); 44 | config.gleam.as_ref().map(print_language_ids); 45 | } 46 | 47 | fn print_language_ids(lc: &LanguageConfig) { 48 | for id in lc.installs.keys() { 49 | println!("{id}") 50 | } 51 | } 52 | 53 | pub fn get_language_config(language: &languages::Language, config: &Config) -> LanguageConfig { 54 | match language { 55 | languages::Language::Gleam => config.gleam.clone().unwrap_or_default(), 56 | languages::Language::Erlang => config.erlang.clone().unwrap_or_default(), 57 | languages::Language::Elixir => config.elixir.clone().unwrap_or_default(), 58 | } 59 | } 60 | 61 | fn get_component_config(kind: &components::Kind, config: &Config) -> ComponentConfig { 62 | match kind { 63 | components::Kind::Elp => config.elp.clone().unwrap_or_default(), 64 | components::Kind::Rebar3 => config.rebar3.clone().unwrap_or_default(), 65 | } 66 | } 67 | 68 | fn get_default_id(lc: &Option) -> Result { 69 | match lc { 70 | None => Err(eyre!("No default found for language {:?}", lc)), 71 | Some(lc) => match &lc.default { 72 | None => Err(eyre!("No default found for language {:?}", lc)), 73 | Some(default) => { 74 | debug!("Found default {:?}", default); 75 | Ok(default.to_string()) 76 | } 77 | }, 78 | } 79 | } 80 | 81 | fn get_component_default_id(c: &Option) -> Result { 82 | match c { 83 | None => Err(eyre!("No default found for component {:?}", c)), 84 | Some(c) => match &c.default { 85 | None => Err(eyre!("No default found for component {:?}", c)), 86 | Some(default) => { 87 | debug!("Found default {:?}", default); 88 | Ok(default.to_string()) 89 | } 90 | }, 91 | } 92 | } 93 | 94 | pub fn switch(language: &languages::Language, id: &str, config: &Config) -> Result<()> { 95 | let language_config = get_language_config(language, config); 96 | 97 | // we just look it up to return an error if it doesn't exist 98 | let _ = lookup_install_by_id(id.to_string(), Some(language_config))?; 99 | 100 | let mut c = match local_config() { 101 | None => toml::Table::new(), 102 | Some(local_config) => local_config.clone(), 103 | }; 104 | 105 | c.insert(language.to_string(), toml::Value::String(id.to_string())); 106 | 107 | let toml_string = toml::to_string(&c).unwrap(); 108 | let mut file = fs::File::create(LOCAL_CONFIG_FILE)?; 109 | file.write_all(toml_string.as_bytes())?; 110 | Ok(()) 111 | } 112 | 113 | fn get_local_id(language_str: String, local_config: &Option) -> Option<&toml::Value> { 114 | match local_config { 115 | None => None, 116 | Some(lc) => lc.get(language_str.clone().as_str()), 117 | } 118 | } 119 | 120 | pub fn get_otp_major_vsn() -> Result { 121 | let dir = match install_to_use_by_language(languages::Language::Erlang) { 122 | Ok(dir) => Ok(dir), 123 | Err(_) => Err(eyre!("No default Erlang installation found. Install an Erlang version, like `beamup install erlang latest` or set a default with `beamup default erlang ` first.")), 124 | }?; 125 | 126 | let releases_dir = Path::new(&dir).join("lib").join("erlang").join("releases"); 127 | match check_release_dir(&releases_dir) { 128 | otp_major_vsn @ Ok(_) => otp_major_vsn, 129 | Err(_) => { 130 | // static Erlang builds have a different structure, so check that too 131 | let releases_dir = Path::new(&dir).join("releases"); 132 | check_release_dir(&releases_dir) 133 | } 134 | } 135 | } 136 | 137 | fn check_release_dir(releases_dir: &Path) -> Result { 138 | for entry in std::fs::read_dir(&releases_dir)? { 139 | let entry = entry?; 140 | let path = entry.path(); 141 | if path.is_dir() { 142 | let binding = entry.file_name(); 143 | let otp_major_vsn = binding 144 | .to_str() 145 | .ok_or(eyre!("Unable to convert OTP vsn {binding:?} to string"))?; 146 | 147 | return Ok(otp_major_vsn.to_string()); 148 | } 149 | } 150 | 151 | Err(eyre!("No installed OTP release found in {releases_dir:?}")) 152 | } 153 | 154 | pub fn component_install_to_use(kind: &components::Kind) -> Result { 155 | let (_, config) = home_config()?; 156 | let component_config = get_component_config(kind, &config); 157 | let local_config = local_config(); 158 | let component_str = kind.to_string(); 159 | 160 | let maybe_id = match get_local_id(component_str, &local_config) { 161 | None => None, 162 | Some(toml::Value::String(id)) => { 163 | debug!("Using id from local config file"); 164 | Some(id) 165 | } 166 | _ => None, 167 | }; 168 | 169 | let id = match maybe_id { 170 | None => { 171 | debug!("No local config found. Using global config"); 172 | match kind { 173 | components::Kind::Elp => get_component_default_id(&config.elp)?, 174 | components::Kind::Rebar3 => get_component_default_id(&config.rebar3)?, 175 | } 176 | } 177 | Some(id) => id.clone(), 178 | }; 179 | 180 | lookup_component_install_by_id(id, Some(component_config)) 181 | } 182 | 183 | pub fn install_to_use_by_bin(bin: &str) -> Result { 184 | let (_, config) = home_config()?; 185 | let language = languages::bin_to_language(bin.to_string(), &config)?; 186 | install_to_use_by_language(language) 187 | } 188 | 189 | fn install_to_use_by_language(language: languages::Language) -> Result { 190 | let (_, config) = home_config()?; 191 | let language_config = get_language_config(&language, &config); 192 | let local_config = local_config(); 193 | let language_str = language.to_string(); 194 | 195 | let maybe_id = match get_local_id(language_str, &local_config) { 196 | None => None, 197 | Some(toml::Value::String(id)) => { 198 | debug!("Using id from local config file"); 199 | Some(id) 200 | } 201 | _ => None, 202 | }; 203 | 204 | let id = match maybe_id { 205 | None => { 206 | debug!("No local config found. Using global config"); 207 | match language { 208 | languages::Language::Gleam => get_default_id(&config.gleam)?, 209 | languages::Language::Erlang => get_default_id(&config.erlang)?, 210 | languages::Language::Elixir => get_default_id(&config.elixir)?, 211 | } 212 | } 213 | Some(id) => id.clone(), 214 | }; 215 | 216 | lookup_install_by_id(id, Some(language_config)) 217 | } 218 | 219 | fn lookup_install_by_id(id: String, lc: Option) -> Result { 220 | debug!("Looking up install for {}", id); 221 | match lc { 222 | None => Err(eyre!("No config found")), 223 | Some(language_config) => match language_config.installs.get(&id) { 224 | None => Err(eyre!("No install found for id {id}")), 225 | // backwards compatible clause for when id's only pointed 226 | // to a directory and not more metadata 227 | Some(toml::Value::String(dir)) => { 228 | debug!("Found install in directory {}", dir); 229 | Ok(dir.to_owned()) 230 | } 231 | Some(t @ toml::Value::Table(_)) => { 232 | if let Some(toml::Value::String(dir)) = t.get("dir") { 233 | Ok(dir.to_string()) 234 | } else { 235 | Err(eyre!("No directory found for install id {id}")) 236 | } 237 | } 238 | _ => Err(eyre!("Bad directory found in installs for id {id}")), 239 | }, 240 | } 241 | } 242 | 243 | fn lookup_component_install_by_id(id: String, lc: Option) -> Result { 244 | debug!("Looking up install for {}", id); 245 | match lc { 246 | None => Err(eyre!("No config found")), 247 | Some(component_config) => match component_config.installs.get(&id) { 248 | None => Err(eyre!("No install found for id {id}")), 249 | // backwards compatible clause for when id's only pointed 250 | // to a directory and not more metadata 251 | Some(toml::Value::String(dir)) => { 252 | debug!("Found install in directory {}", dir); 253 | Ok(dir.to_owned()) 254 | } 255 | Some(t @ toml::Value::Table(_)) => { 256 | if let Some(toml::Value::String(dir)) = t.get("dir") { 257 | Ok(dir.to_string()) 258 | } else { 259 | Err(eyre!("No directory found for install id {id}")) 260 | } 261 | } 262 | _ => Err(eyre!("Bad directory found in installs for id {id}")), 263 | }, 264 | } 265 | } 266 | 267 | pub fn lookup_default_build_options(language: &languages::Language, config: &Config) -> String { 268 | debug!("Looking up default configure options for {:?}", language); 269 | 270 | let lc = get_language_config(language, config); 271 | 272 | match lc.default_build_options { 273 | None => "".to_string(), 274 | Some(options) => options.to_owned(), 275 | } 276 | } 277 | 278 | pub fn set_default( 279 | language: &languages::Language, 280 | id: &String, 281 | config_file: String, 282 | config: Config, 283 | ) -> Result<(), Report> { 284 | debug!("set default {:?} to use to {:?}", language, id); 285 | let lc = get_language_config(language, &config); 286 | let LanguageConfig { 287 | default: _, 288 | installs: installs_table, 289 | default_build_options, 290 | } = lc; 291 | 292 | let new_lc = LanguageConfig { 293 | default: Some(id.to_owned()), 294 | installs: installs_table.clone(), 295 | default_build_options: default_build_options.clone(), 296 | }; 297 | 298 | let new_config = match language { 299 | languages::Language::Gleam => Config { 300 | gleam: Some(new_lc), 301 | ..config 302 | }, 303 | languages::Language::Erlang => Config { 304 | erlang: Some(new_lc), 305 | ..config 306 | }, 307 | languages::Language::Elixir => Config { 308 | elixir: Some(new_lc), 309 | ..config 310 | }, 311 | }; 312 | 313 | write_config(config_file, new_config) 314 | } 315 | 316 | pub fn update_language_config( 317 | language: &languages::Language, 318 | id: &String, 319 | release: &String, 320 | dir: String, 321 | lc: LanguageConfig, 322 | ) -> Result { 323 | let LanguageConfig { 324 | default: _, 325 | installs: mut table, 326 | default_build_options, 327 | } = lc; 328 | let mut id_table = toml::Table::new(); 329 | id_table.insert("dir".to_string(), toml::Value::String(dir)); 330 | id_table.insert( 331 | "release".to_string(), 332 | toml::Value::String(release.to_owned()), 333 | ); 334 | 335 | if language == &languages::Language::Elixir { 336 | let otp_vsn = get_otp_major_vsn()?; 337 | id_table.insert("otp_vsn".to_string(), toml::Value::String(otp_vsn)); 338 | } 339 | 340 | table.insert(id.clone(), toml::Value::Table(id_table)); 341 | Ok(LanguageConfig { 342 | default: Some(id.to_owned()), 343 | installs: table.clone(), 344 | default_build_options: default_build_options.clone(), 345 | }) 346 | } 347 | 348 | pub fn update_component_config( 349 | _kind: &components::Kind, 350 | id: &String, 351 | release: &String, 352 | dir: String, 353 | c: ComponentConfig, 354 | ) -> Result { 355 | let ComponentConfig { 356 | default: _, 357 | installs: mut table, 358 | default_build_options, 359 | } = c; 360 | let mut id_table = toml::Table::new(); 361 | id_table.insert("dir".to_string(), toml::Value::String(dir)); 362 | id_table.insert( 363 | "release".to_string(), 364 | toml::Value::String(release.to_owned()), 365 | ); 366 | 367 | table.insert(id.clone(), toml::Value::Table(id_table)); 368 | Ok(ComponentConfig { 369 | default: Some(id.to_owned()), 370 | installs: table.clone(), 371 | default_build_options: default_build_options.clone(), 372 | }) 373 | } 374 | 375 | pub fn add_install( 376 | language: &languages::Language, 377 | id: &String, 378 | release: &String, 379 | dir: String, 380 | config_file: String, 381 | config: Config, 382 | ) -> Result<()> { 383 | debug!("adding install {id} pointing to {dir}"); 384 | let language_config = get_language_config(language, &config); 385 | 386 | let updated_language_config = 387 | update_language_config(language, id, release, dir, language_config.clone())?; 388 | 389 | let new_config = match language { 390 | languages::Language::Gleam => Config { 391 | gleam: Some(updated_language_config), 392 | ..config 393 | }, 394 | languages::Language::Erlang => Config { 395 | erlang: Some(updated_language_config), 396 | ..config 397 | }, 398 | languages::Language::Elixir => Config { 399 | elixir: Some(updated_language_config), 400 | ..config 401 | }, 402 | }; 403 | 404 | let _ = write_config(config_file, new_config); 405 | 406 | Ok(()) 407 | } 408 | 409 | pub fn add_component_install( 410 | kind: &components::Kind, 411 | id: &String, 412 | release: &String, 413 | dir: String, 414 | config_file: String, 415 | config: Config, 416 | ) -> Result<()> { 417 | debug!("adding install {id} pointing to {dir}"); 418 | let component_config = get_component_config(kind, &config); 419 | 420 | let updated_component_config = 421 | update_component_config(kind, id, release, dir, component_config.clone())?; 422 | 423 | let new_config = match kind { 424 | components::Kind::Elp => Config { 425 | elp: Some(updated_component_config), 426 | ..config 427 | }, 428 | components::Kind::Rebar3 => Config { 429 | rebar3: Some(updated_component_config), 430 | ..config 431 | }, 432 | }; 433 | 434 | let _ = write_config(config_file, new_config); 435 | 436 | Ok(()) 437 | } 438 | 439 | pub fn maybe_create_dir(release_dir: &PathBuf, force: bool) -> Result<()> { 440 | match release_dir.try_exists() { 441 | Ok(true) => 442 | match force { 443 | true => { 444 | info!("Force enabled. Deleting existing release directory {release_dir:?}"); 445 | fs::remove_dir_all(release_dir)? 446 | }, 447 | _ => return Err(eyre!("Release directory already exists. Use `-f` to delete {release_dir:?} and recreate instead of giving this error.")), 448 | } 449 | Ok(false) => {}, 450 | Err(e) => return Err(eyre!( 451 | "Unable to check for existence of release directory: {e:?}" 452 | )), 453 | }; 454 | 455 | let _ = std::fs::create_dir_all(release_dir); 456 | 457 | Ok(()) 458 | } 459 | 460 | pub fn bin_dir() -> PathBuf { 461 | match dirs::executable_dir() { 462 | Some(bin_dir) => bin_dir, 463 | None => { 464 | let home_dir = dirs::home_dir().unwrap(); 465 | Path::new(&home_dir).join(".beamup").join("bin") 466 | } 467 | } 468 | } 469 | 470 | pub fn data_dir() -> Result { 471 | match dirs::data_local_dir() { 472 | Some(dir) => Ok(dir), 473 | None => Err(eyre!("No data directory available")), 474 | } 475 | } 476 | 477 | pub fn home_config_file() -> Result { 478 | let config_dir = match dirs::config_local_dir() { 479 | Some(d) => d, 480 | None => return Err(eyre!("no home directory available")), 481 | }; 482 | let data_dir = match dirs::data_local_dir() { 483 | Some(d) => d, 484 | None => return Err(eyre!("no home directory available")), 485 | }; 486 | 487 | let default_config = config_dir.join("beamup").join(CONFIG_FILE); 488 | let default_data = data_dir.join("beamup"); 489 | 490 | let _ = fs::create_dir_all(config_dir.join("beamup")); 491 | let _ = fs::create_dir_all(data_dir.join("beamup")); 492 | 493 | if !default_config.exists() { 494 | let config = Config { 495 | install_dir: default_data.to_str().unwrap().to_string(), 496 | erlang: Some(LanguageConfig { 497 | default: None, 498 | installs: toml::Table::new(), 499 | default_build_options: None, 500 | }), 501 | gleam: Some(LanguageConfig { 502 | default: None, 503 | installs: toml::Table::new(), 504 | default_build_options: None, 505 | }), 506 | elixir: Some(LanguageConfig { 507 | default: None, 508 | installs: toml::Table::new(), 509 | default_build_options: None, 510 | }), 511 | elp: Some(ComponentConfig { 512 | default: None, 513 | installs: toml::Table::new(), 514 | default_build_options: None, 515 | }), 516 | rebar3: Some(ComponentConfig { 517 | default: None, 518 | installs: toml::Table::new(), 519 | default_build_options: None, 520 | }), 521 | }; 522 | 523 | write_config(default_config.to_str().unwrap().to_string(), config)?; 524 | info!( 525 | "Created a default config at {:?}", 526 | default_config.to_owned() 527 | ); 528 | } 529 | 530 | Ok(default_config.to_str().unwrap().to_string()) 531 | } 532 | 533 | fn local_config() -> Option { 534 | match fs::read_to_string(LOCAL_CONFIG_FILE) { 535 | Ok(local_config_str) => toml::from_str(local_config_str.as_str()).ok(), 536 | _ => None, 537 | } 538 | } 539 | 540 | pub fn home_config() -> Result<(String, Config)> { 541 | let config_file = home_config_file()?; 542 | Ok((config_file.to_owned(), read_config(config_file))) 543 | } 544 | 545 | pub fn read_config(file: String) -> Config { 546 | let toml_str = fs::read_to_string(file).expect("Failed to read config file"); 547 | let config: Config = toml::from_str(toml_str.as_str()).unwrap(); 548 | config 549 | } 550 | 551 | pub fn write_config(file_path: String, config: Config) -> Result<()> { 552 | let toml_string = toml::to_string(&config).unwrap(); 553 | let mut file = fs::File::create(file_path)?; 554 | file.write_all(toml_string.as_bytes())?; 555 | Ok(()) 556 | } 557 | --------------------------------------------------------------------------------