├── LICENSE ├── README.md ├── bin ├── compile ├── detect ├── release ├── test └── test-compile ├── elixir_buildpack.config ├── lib ├── app_funcs.sh ├── canonical_version.sh ├── elixir_funcs.sh ├── erlang_funcs.sh ├── misc_funcs.sh └── path_funcs.sh └── test └── canonical_version_test.sh /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Akash Manohar J 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Heroku Buildpack for Elixir 2 | 3 | ## Features 4 | 5 | * **Easy configuration** with `elixir_buildpack.config` file 6 | * Use **prebuilt Elixir binaries** 7 | * Allows configuring Erlang 8 | * If your app doesn't have a Procfile, default web task `mix run --no-halt` will be run. 9 | * Consolidates protocols 10 | * Hex and rebar support 11 | * Caching of Hex packages, Mix dependencies and downloads 12 | * Compilation procedure hooks through `hook_pre_compile`, `hook_compile`, `hook_post_compile` configuration 13 | 14 | #### Version support 15 | 16 | * Erlang - Prebuilt packages (17.5, 17.4, etc) 17 | * The full list of prebuilt packages can be found here: 18 | * gigalixir-20 or heroku-20 stacks: https://builds.hex.pm/builds/otp/ubuntu-20.04/builds.txt 19 | * heroku-22 stacks: https://builds.hex.pm/builds/otp/ubuntu-22.04/builds.txt 20 | * All other stacks: https://github.com/HashNuke/heroku-buildpack-elixir-otp-builds/blob/master/otp-versions 21 | * Elixir - Prebuilt releases (1.0.4, 1.0.3, etc) or prebuilt branches (master, v1.7, etc) 22 | * The full list of releases can be found here: https://github.com/elixir-lang/elixir/releases 23 | * The full list of branches can be found here: https://github.com/elixir-lang/elixir/branches 24 | 25 | Note: you should choose an Elixir and Erlang version that are [compatible with one another](https://hexdocs.pm/elixir/compatibility-and-deprecations.html#compatibility-between-elixir-and-erlang-otp). 26 | 27 | #### Cloud Native Support 28 | 29 | * Cloud Native users should use [this buildpack](https://github.com/elixir-buildpack/cloud-native-buildpack) 30 | 31 | **This buildpack is not guaranteed to be Cloud Native compatible.** 32 | The [elixir-buildpack/cloud-native-buildpack](https://github.com/elixir-buildpack/cloud-native-buildpack) is a buildpack that is actively under development 33 | and is designed specifically to follow the Cloud Native Buildpack conventions. 34 | 35 | ## Usage 36 | 37 | #### Create a Heroku app with this buildpack 38 | 39 | ``` 40 | heroku create --buildpack hashnuke/elixir 41 | ``` 42 | 43 | #### Set the buildpack for an existing Heroku app 44 | 45 | ``` 46 | heroku buildpacks:set hashnuke/elixir 47 | ``` 48 | 49 | #### Use the edge version of buildpack 50 | 51 | The `hashnuke/elixir` buildpack contains the latest published version of the buildpack, but you can use the edge version (i.e. the source code in this repo) by running: 52 | 53 | ``` 54 | heroku buildpacks:set https://github.com/HashNuke/heroku-buildpack-elixir.git 55 | ``` 56 | 57 | When you decide to use the published or the edge version of the buildpack you should be aware that, although we attempt to maintain the buildpack for as many old Elixir and Erlang releases as possible, it is sometimes difficult since there's a matrix of 3 variables involved: Erlang version, Elixir version and Heroku stack. If your application cannot be updated for some reason and requires an older version of the buildpack then [use a specific version of buildpack](#use-a-specific-version-of-buildpack). 58 | 59 | #### Use a specific version of buildpack 60 | 61 | The methods above always use the latest version of the buildpack code. To use a specific version of the buildpack, choose a commit from the [commits](https://github.com/HashNuke/heroku-buildpack-elixir/commits/master) page. The commit SHA forms part of your buildpack url. 62 | 63 | For example, if you pick the commit ["883f33e10879b4b8b030753c13aa3d0dda82e1e7"](https://github.com/HashNuke/heroku-buildpack-elixir/commit/883f33e10879b4b8b030753c13aa3d0dda82e1e7), then the buildpack url for your app would be: 64 | 65 | ``` 66 | https://github.com/HashNuke/heroku-buildpack-elixir.git#883f33e10879b4b8b030753c13aa3d0dda82e1e7 67 | ``` 68 | 69 | **It is recommended to use a buildpack url with a commit SHA on production apps.** This prevents the unpleasant moment when your Heroku build fails because the buildpack you use just got updated with a breaking change. Having buildpacks pinned to a specific version is like having your Hex packages pinned to a specific version in `mix.lock`. 70 | 71 | #### Using Heroku CI 72 | 73 | This buildpack supports Heroku CI. 74 | 75 | * To enable viewing test runs on Heroku, add [tapex](https://github.com/joshwlewis/tapex) to your project. 76 | * To detect compilation warnings use the `hook_compile` configuration option set to `mix compile --force --warnings-as-errors`. 77 | 78 | #### Elixir Releases 79 | 80 | This buildpack can optionally build an [Elixir release](https://hexdocs.pm/mix/Mix.Tasks.Release.html). The release build will be run after `hook_post_compile`. 81 | 82 | WARNING: If you need to do further compilation using another buildpack, such as the [Phoenix static buildpack](https://github.com/gjaldon/heroku-buildpack-phoenix-static), you probably don't want to use this option. See the [Elixir release buildpack](https://github.com/chrismcg/heroku-buildpack-elixir-mix-release) instead. 83 | 84 | To build and use a release for an app called `foo` compiled with `MIX_ENV=prod`: 85 | 1. Make sure `elixir_version` in `elixir_buildpack.config` is at least 1.9 86 | 2. Add `release=true` to `elixir_buildpack.config` 87 | 3. Use `web: _build/prod/rel/foo/bin/foo start` in your Procfile 88 | 89 | NOTE: This requires the master version of the buildpack (or a commit later than 7d369c) 90 | 91 | ## Configuration 92 | 93 | Create a `elixir_buildpack.config` file in your app's root dir. The file's syntax is bash. 94 | 95 | If you don't specify a config option, then the default option from the buildpack's [`elixir_buildpack.config`](https://github.com/HashNuke/heroku-buildpack-elixir/blob/master/elixir_buildpack.config) file will be used. 96 | 97 | 98 | __Here's a full config file with all available options:__ 99 | 100 | ``` 101 | # Erlang version 102 | erlang_version=18.2.1 103 | 104 | # Elixir version 105 | elixir_version=1.2.0 106 | 107 | # Always rebuild from scratch on every deploy? 108 | always_rebuild=false 109 | 110 | # Create a release using `mix release`? (requires Elixir 1.9) 111 | release=true 112 | 113 | # A command to run right before fetching dependencies 114 | hook_pre_fetch_dependencies="pwd" 115 | 116 | # A command to run right before compiling the app (after elixir, .etc) 117 | hook_pre_compile="pwd" 118 | 119 | hook_compile="mix compile --force --warnings-as-errors" 120 | 121 | # A command to run right after compiling the app 122 | hook_post_compile="pwd" 123 | 124 | # Set the path the app is run from 125 | runtime_path=/app 126 | 127 | # Enable or disable additional test arguments 128 | test_args="--cover" 129 | ``` 130 | 131 | 132 | #### Migrating from previous build pack 133 | the following has been deprecated and should be removed from `elixir_buildpack.config`: 134 | ``` 135 | # Export heroku config vars 136 | config_vars_to_export=(DATABASE_URL) 137 | ``` 138 | 139 | #### Specifying Elixir version 140 | 141 | * Use prebuilt Elixir release 142 | 143 | ``` 144 | elixir_version=1.2.0 145 | ``` 146 | 147 | * Use prebuilt Elixir branch, the *branch* specifier ensures that it will be downloaded every time 148 | 149 | ``` 150 | elixir_version=(branch master) 151 | ``` 152 | 153 | #### Specifying Erlang version 154 | 155 | * You can specify an Erlang release version like below 156 | 157 | ``` 158 | erlang_version=18.2.1 159 | ``` 160 | 161 | #### Specifying config vars to export at compile time 162 | 163 | * To set a config var on your heroku node you can exec from the shell: 164 | 165 | ``` 166 | heroku config:set MY_VAR=the_value 167 | ``` 168 | 169 | ## Other notes 170 | 171 | * Add your own `Procfile` to your application, else the default web task `mix run --no-halt` will be used. 172 | 173 | * Your application should build embedded and start permanent. Build embedded will consolidate protocols for a performance boost, start permanent will ensure that Heroku restarts your application if it crashes. See below for an example of how to use these features in your Mix project: 174 | 175 | ```elixir 176 | defmodule MyApp.Mixfile do 177 | use Mix.Project 178 | 179 | def project do 180 | [app: :my_app, 181 | version: "0.0.1", 182 | build_embedded: Mix.env == :prod, 183 | start_permanent: Mix.env == :prod] 184 | end 185 | end 186 | ``` 187 | 188 | * The buildpack will execute the commands configured in `hook_pre_compile` and/or `hook_post_compile` in the root directory of your application before/after it has been compiled (respectively). These scripts can be used to build or prepare things for your application, for example compiling assets. 189 | * The buildpack will execute the commands configured in `hook_pre_fetch_dependencies` in the root directory of your application before it fetches the application dependencies. This script can be used to clean certain dependencies before fetching new ones. 190 | 191 | ## Development 192 | 193 | * Build scripts to build erlang are at 194 | * Sample app to test is available at 195 | 196 | ## Testing 197 | 198 | To run tests 199 | ``` 200 | git clone https://github.com/HashNuke/heroku-buildpack-elixir 201 | export BUILDPACK="$(pwd)/heroku-buildpack-elixir" 202 | git clone https://github.com/jesseshieh/heroku-buildpack-testrunner 203 | git clone https://github.com/jesseshieh/shunit2 204 | export SHUNIT_HOME="$(pwd)/shunit2" 205 | cd heroku-buildpack-testrunner 206 | bin/run $BUILDPACK 207 | ``` 208 | 209 | See more info at https://github.com/jesseshieh/heroku-buildpack-testrunner/blob/master/README.md 210 | 211 | ## Credits 212 | 213 | © Akash Manohar under The MIT License. Feel free to do whatever you want with it. 214 | -------------------------------------------------------------------------------- /bin/compile: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e -o pipefail 4 | 5 | # If this var is set to true later on, 6 | # then elixir and rebar will be rebuilt 7 | erlang_changed=false 8 | rebar_changed=false 9 | elixir_changed=false 10 | 11 | build_pack_path=$(cd $(dirname $(dirname $0)); pwd) 12 | 13 | # Ensure dirs are present 14 | mkdir -p $1 $2 $3 15 | 16 | build_path=$(cd $1 && pwd) 17 | cache_path=$(cd $2 && pwd) 18 | env_path=$(cd $3 && pwd) 19 | 20 | 21 | source ${build_pack_path}/lib/path_funcs.sh 22 | source ${build_pack_path}/lib/misc_funcs.sh 23 | source ${build_pack_path}/lib/erlang_funcs.sh 24 | source ${build_pack_path}/lib/elixir_funcs.sh 25 | source ${build_pack_path}/lib/app_funcs.sh 26 | source ${build_pack_path}/lib/canonical_version.sh 27 | 28 | mkdir $(build_platform_tools_path) 29 | 30 | export_env_vars 31 | export_mix_env 32 | export_mix_home 33 | export_hex_home 34 | load_config 35 | check_erlang_version "$erlang_version" 36 | check_elixir_version "$elixir_version" 37 | 38 | check_stack 39 | clean_cache 40 | 41 | download_erlang 42 | install_erlang 43 | 44 | download_elixir 45 | install_elixir 46 | restore_mix 47 | install_hex 48 | install_rebar 49 | 50 | # deprecated_hook, here for backwards compatibility 51 | pre_compile_hook 52 | 53 | restore_app 54 | hook_pre_app_dependencies 55 | app_dependencies 56 | copy_hex 57 | 58 | hook_pre_compile 59 | compile_app 60 | hook_post_compile 61 | 62 | release_app 63 | 64 | backup_app 65 | backup_mix 66 | write_profile_d_script 67 | write_export 68 | 69 | # deprecated_hook, here for backwards compatibility 70 | post_compile_hook 71 | -------------------------------------------------------------------------------- /bin/detect: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e -o pipefail 4 | 5 | mix_file_url="$1/mix.exs" 6 | 7 | if [ -f $mix_file_url ]; 8 | then 9 | echo "Elixir" 10 | exit 0 11 | else 12 | exit 1 13 | fi 14 | -------------------------------------------------------------------------------- /bin/release: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | cat < /dev/null 39 | } 40 | 41 | function hook_pre_compile() { 42 | cd $build_path 43 | 44 | if [ -n "$hook_pre_compile" ]; then 45 | output_section "Executing hook before compile: $hook_pre_compile" 46 | $hook_pre_compile || exit 1 47 | fi 48 | 49 | cd - > /dev/null 50 | } 51 | 52 | function hook_post_compile() { 53 | cd $build_path 54 | 55 | if [ -n "$hook_post_compile" ]; then 56 | output_section "Executing hook after compile: $hook_post_compile" 57 | $hook_post_compile || exit 1 58 | fi 59 | 60 | cd - > /dev/null 61 | } 62 | 63 | function app_dependencies() { 64 | mkdir -p "$(build_mix_home_path)" 65 | 66 | cd $build_path 67 | output_section "Fetching app dependencies with mix" 68 | 69 | # Unset this var so that if the parent dir is a git repo, it isn't detected 70 | # And all git operations are performed on the respective repos 71 | env \ 72 | -u GIT_DIR \ 73 | mix deps.get --only $MIX_ENV || exit 1 74 | 75 | cd - > /dev/null 76 | } 77 | 78 | 79 | function backup_app() { 80 | # Delete the previous backups 81 | rm -rf $(deps_backup_path) $(build_backup_path) 82 | 83 | mkdir -p $(deps_backup_path) $(build_backup_path) 84 | cp -pR ${build_path}/deps/* $(deps_backup_path) 85 | cp -pR ${build_path}/_build/* $(build_backup_path) 86 | } 87 | 88 | 89 | function compile_app() { 90 | local git_dir_value=$GIT_DIR 91 | unset GIT_DIR 92 | 93 | cd $build_path 94 | output_section "Compiling" 95 | 96 | if [ -n "$hook_compile" ]; then 97 | output_section "(using custom compile command)" 98 | $hook_compile || exit 1 99 | else 100 | mix compile --force || exit 1 101 | fi 102 | 103 | mix deps.clean --unused 104 | 105 | export GIT_DIR=$git_dir_value 106 | cd - > /dev/null 107 | } 108 | 109 | function release_app() { 110 | cd $build_path 111 | 112 | if [ $release = true ]; then 113 | output_section "Building release" 114 | mix release --overwrite 115 | fi 116 | 117 | cd - > /dev/null 118 | } 119 | 120 | function post_compile_hook() { 121 | cd $build_path 122 | 123 | if [ -n "$post_compile" ]; then 124 | output_section "Executing DEPRECATED post compile: $post_compile" 125 | $post_compile || exit 1 126 | fi 127 | 128 | cd - > /dev/null 129 | } 130 | 131 | function pre_compile_hook() { 132 | cd $build_path 133 | 134 | if [ -n "$pre_compile" ]; then 135 | output_section "Executing DEPRECATED pre compile: $pre_compile" 136 | $pre_compile || exit 1 137 | fi 138 | 139 | cd - > /dev/null 140 | } 141 | 142 | function export_var() { 143 | local VAR_NAME=$1 144 | local VAR_VALUE=$2 145 | 146 | echo "export ${VAR_NAME}=${VAR_VALUE}" 147 | } 148 | 149 | function export_default_var() { 150 | local VAR_NAME=$1 151 | local DEFAULT_VALUE=$2 152 | 153 | if [ ! -f "${env_path}/${VAR_NAME}" ]; then 154 | export_var "${VAR_NAME}" "${DEFAULT_VALUE}" 155 | fi 156 | } 157 | 158 | function echo_profile_env_vars() { 159 | local buildpack_bin="$(runtime_platform_tools_path)" 160 | buildpack_bin="$(runtime_erlang_path)/bin:${buildpack_bin}" 161 | buildpack_bin="$(runtime_elixir_path)/bin:${buildpack_bin}" 162 | 163 | 164 | export_var "PATH" "${buildpack_bin}:\$PATH" 165 | export_default_var "LC_CTYPE" "en_US.utf8" 166 | 167 | # Only write MIX_* to profile if the application did not set MIX_* 168 | export_default_var "MIX_ENV" "${MIX_ENV}" 169 | export_default_var "MIX_HOME" "$(runtime_mix_home_path)" 170 | export_default_var "HEX_HOME" "$(runtime_hex_home_path)" 171 | } 172 | 173 | function echo_export_env_vars() { 174 | local buildpack_bin="$(build_platform_tools_path)" 175 | buildpack_bin="$(build_erlang_path)/bin:${buildpack_bin}" 176 | buildpack_bin="$(build_elixir_path)/bin:${buildpack_bin}" 177 | 178 | 179 | export_var "PATH" "${buildpack_bin}:\$PATH" 180 | export_default_var "LC_CTYPE" "en_US.utf8" 181 | 182 | # Only write MIX_* to profile if the application did not set MIX_* 183 | export_default_var "MIX_ENV" "${MIX_ENV}" 184 | export_default_var "MIX_HOME" "$(build_mix_home_path)" 185 | export_default_var "HEX_HOME" "$(build_hex_home_path)" 186 | } 187 | 188 | function write_profile_d_script() { 189 | output_section "Creating .profile.d with env vars" 190 | mkdir -p $build_path/.profile.d 191 | local profile_path="${build_path}/.profile.d/elixir_buildpack_paths.sh" 192 | 193 | echo_profile_env_vars >> $profile_path 194 | } 195 | 196 | function write_export() { 197 | output_section "Writing export for multi-buildpack support" 198 | 199 | echo_export_env_vars >> "${build_pack_path}/export" 200 | } 201 | -------------------------------------------------------------------------------- /lib/canonical_version.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | erlang_builds_url() { 4 | case "${STACK}" in 5 | "heroku-20") 6 | erlang_builds_url="https://builds.hex.pm/builds/otp/ubuntu-20.04" 7 | ;; 8 | "heroku-22") 9 | erlang_builds_url="https://builds.hex.pm/builds/otp/ubuntu-22.04" 10 | ;; 11 | *) 12 | erlang_builds_url="https://s3.amazonaws.com/heroku-buildpack-elixir/erlang/cedar-14" 13 | ;; 14 | esac 15 | echo $erlang_builds_url 16 | } 17 | 18 | fetch_elixir_versions() { 19 | url="https://builds.hex.pm/builds/elixir/builds.txt" 20 | curl -s "$url" | awk '/^v[0-9.]+[- ]/ { print $1 }' 21 | } 22 | 23 | fetch_erlang_versions() { 24 | case "${STACK}" in 25 | "heroku-20") 26 | url="https://builds.hex.pm/builds/otp/ubuntu-20.04/builds.txt" 27 | curl -s "$url" | awk '/^OTP-([0-9.]+ )/ {print substr($1,5)}' 28 | ;; 29 | "heroku-22") 30 | url="https://builds.hex.pm/builds/otp/ubuntu-22.04/builds.txt" 31 | curl -s "$url" | awk '/^OTP-([0-9.]+ )/ {print substr($1,5)}' 32 | ;; 33 | *) 34 | url="https://raw.githubusercontent.com/HashNuke/heroku-buildpack-elixir-otp-builds/master/otp-versions" 35 | curl -s "$url" 36 | ;; 37 | esac 38 | } 39 | 40 | exact_erlang_version_available() { 41 | # TODO: fallback to hashnuke one if not ubuntu-20.04 and not found on hex 42 | version=$1 43 | available_versions=$2 44 | found=1 45 | while read -r line; do 46 | if [ "$line" = "$version" ]; then 47 | found=0 48 | fi 49 | done <<< "$available_versions" 50 | echo $found 51 | } 52 | 53 | exact_elixir_version_available() { 54 | version=$1 55 | available_versions=$2 56 | found=1 57 | while read -r line; do 58 | if [ "$line" = "$version" ]; then 59 | found=0 60 | fi 61 | done <<< "$available_versions" 62 | echo $found 63 | } 64 | 65 | check_erlang_version() { 66 | version=$1 67 | exists=$(exact_erlang_version_available "$version" "$(fetch_erlang_versions)") 68 | if [ $exists -ne 0 ]; then 69 | output_line "Sorry, Erlang '$version' isn't supported yet or isn't formatted correctly. For a list of supported versions, please see https://github.com/HashNuke/heroku-buildpack-elixir#version-support" 70 | exit 1 71 | fi 72 | } 73 | 74 | check_elixir_version() { 75 | version=$1 76 | exists=$(exact_elixir_version_available "$version" "$(fetch_elixir_versions)") 77 | if [ $exists -ne 0 ]; then 78 | output_line "Sorry, Elixir '$version' isn't supported yet or isn't formatted correctly. For a list of supported versions, please see https://github.com/HashNuke/heroku-buildpack-elixir#version-support" 79 | exit 1 80 | fi 81 | } 82 | -------------------------------------------------------------------------------- /lib/elixir_funcs.sh: -------------------------------------------------------------------------------- 1 | function download_elixir() { 2 | # If a previous download does not exist, then always re-download 3 | mkdir -p $(elixir_cache_path) 4 | 5 | if [ ${force_fetch} = true ] || [ ! -f $(elixir_cache_path)/$(elixir_download_file) ]; then 6 | clean_elixir_downloads 7 | elixir_changed=true 8 | local otp_version=$(otp_version ${erlang_version}) 9 | 10 | local download_url="https://builds.hex.pm/builds/elixir/${elixir_version}-otp-${otp_version}.zip" 11 | 12 | output_section "Fetching Elixir ${elixir_version} for OTP ${otp_version} from ${download_url}" 13 | 14 | curl -s ${download_url} -o $(elixir_cache_path)/$(elixir_download_file) 15 | 16 | if [ $? -ne 0 ]; then 17 | output_section "Falling back to fetching Elixir ${elixir_version} for generic OTP version" 18 | local download_url="https://builds.hex.pm/builds/elixir/${elixir_version}.zip" 19 | curl -s ${download_url} -o $(elixir_cache_path)/$(elixir_download_file) || exit 1 20 | fi 21 | else 22 | output_section "Using cached Elixir ${elixir_version}" 23 | fi 24 | } 25 | 26 | function install_elixir() { 27 | output_section "Installing Elixir ${elixir_version} $(elixir_changed)" 28 | 29 | mkdir -p $(build_elixir_path) 30 | 31 | cd $(build_elixir_path) 32 | 33 | if type "unzip" &> /dev/null; then 34 | unzip -q $(elixir_cache_path)/$(elixir_download_file) 35 | else 36 | jar xf $(elixir_cache_path)/$(elixir_download_file) 37 | fi 38 | 39 | cd - > /dev/null 40 | 41 | if [ $(build_elixir_path) != $(runtime_elixir_path) ]; then 42 | mkdir -p $(runtime_elixir_path) 43 | cp -R $(build_elixir_path)/* $(runtime_elixir_path) 44 | fi 45 | 46 | chmod +x $(build_elixir_path)/bin/* 47 | PATH=$(build_elixir_path)/bin:${PATH} 48 | 49 | export LC_CTYPE=en_US.utf8 50 | } 51 | 52 | function elixir_download_file() { 53 | local otp_version=$(otp_version ${erlang_version}) 54 | echo elixir-${elixir_version}-otp-${otp_version}.zip 55 | } 56 | 57 | function clean_elixir_downloads() { 58 | rm -rf $(elixir_cache_path) 59 | mkdir -p $(elixir_cache_path) 60 | } 61 | 62 | function restore_mix() { 63 | if [ -d $(mix_backup_path) ]; then 64 | mkdir -p $(build_mix_home_path) 65 | cp -pR $(mix_backup_path)/* $(build_mix_home_path) 66 | fi 67 | 68 | if [ -d $(hex_backup_path) ]; then 69 | mkdir -p $(build_hex_home_path) 70 | cp -pR $(hex_backup_path)/* $(build_hex_home_path) 71 | fi 72 | } 73 | 74 | function backup_mix() { 75 | # Delete the previous backups 76 | rm -rf $(mix_backup_path) $(hex_backup_path) 77 | 78 | mkdir -p $(mix_backup_path) $(hex_backup_path) 79 | 80 | cp -pR $(build_mix_home_path)/* $(mix_backup_path) 81 | cp -pR $(build_hex_home_path)/* $(hex_backup_path) 82 | 83 | # https://github.com/HashNuke/heroku-buildpack-elixir/issues/194 84 | if [ $(build_hex_home_path) != $(runtime_hex_home_path) ]; then 85 | mkdir -p $(runtime_hex_home_path) 86 | cp -pR $(build_hex_home_path)/* $(runtime_hex_home_path) 87 | fi 88 | 89 | # https://github.com/HashNuke/heroku-buildpack-elixir/issues/194 90 | if [ $(build_mix_home_path) != $(runtime_mix_home_path) ]; then 91 | mkdir -p $(runtime_mix_home_path) 92 | cp -pR $(build_mix_home_path)/* $(runtime_mix_home_path) 93 | fi 94 | } 95 | 96 | function install_hex() { 97 | output_section "Installing Hex" 98 | mix local.hex --force 99 | } 100 | 101 | function install_rebar() { 102 | output_section "Installing rebar" 103 | 104 | mix local.rebar --force 105 | } 106 | 107 | function elixir_changed() { 108 | if [ $elixir_changed = true ]; then 109 | echo "(changed)" 110 | clean_elixir_version_dependent_cache 111 | fi 112 | } 113 | 114 | function otp_version() { 115 | echo $(echo "$1" | awk 'match($0, /^[0-9][0-9]/) { print substr( $0, RSTART, RLENGTH )}') 116 | } 117 | -------------------------------------------------------------------------------- /lib/erlang_funcs.sh: -------------------------------------------------------------------------------- 1 | function erlang_tarball() { 2 | echo "OTP-${erlang_version}.tar.gz" 3 | } 4 | 5 | function download_erlang() { 6 | mkdir -p $(erlang_cache_path) 7 | erlang_package_url="$(erlang_builds_url)/$(erlang_tarball)" 8 | 9 | # If a previous download does not exist, then always re-download 10 | if [ ! -f $(erlang_cache_path)/$(erlang_tarball) ]; then 11 | clean_erlang_downloads 12 | 13 | # Set this so elixir will be force-rebuilt 14 | erlang_changed=true 15 | 16 | output_section "Fetching Erlang ${erlang_version} from ${erlang_package_url}" 17 | curl -s ${erlang_package_url} -o $(erlang_cache_path)/$(erlang_tarball) || exit 1 18 | else 19 | output_section "Using cached Erlang ${erlang_version}" 20 | fi 21 | } 22 | 23 | function clean_erlang_downloads() { 24 | rm -rf $(erlang_cache_path) 25 | mkdir -p $(erlang_cache_path) 26 | } 27 | 28 | function install_erlang() { 29 | output_section "Installing Erlang ${erlang_version} $(erlang_changed)" 30 | 31 | local tmp_path=$(mktemp -d) 32 | 33 | tar zxf $(erlang_cache_path)/$(erlang_tarball) -C "${tmp_path}" --strip-components=1 34 | 35 | rm -rf $(runtime_erlang_path) 36 | mkdir -p $(runtime_platform_tools_path) 37 | ln -s ${tmp_path} $(runtime_erlang_path) 38 | ${tmp_path}/Install -minimal $(runtime_erlang_path) 39 | 40 | # remove symlink so we can copy into the BUILD_DIR without symlinks 41 | rm $(runtime_erlang_path) 42 | mkdir -p $(runtime_erlang_path) 43 | cp -R ${tmp_path}/* $(runtime_erlang_path) 44 | 45 | # only copy if using old build system; 46 | # newer versions of the build system run builds with BUILD_PATH=/app 47 | # https://github.com/HashNuke/heroku-buildpack-elixir/issues/194#issuecomment-800425532 48 | if [ $(build_erlang_path) != $(runtime_erlang_path) ]; then 49 | mkdir -p $(build_erlang_path) 50 | cp -R $(runtime_erlang_path)/* $(build_erlang_path) 51 | fi 52 | 53 | PATH=$(runtime_erlang_path)/bin:$PATH 54 | } 55 | 56 | function erlang_changed() { 57 | if [ $erlang_changed = true ]; then 58 | echo "(changed)" 59 | fi 60 | } 61 | -------------------------------------------------------------------------------- /lib/misc_funcs.sh: -------------------------------------------------------------------------------- 1 | # Outputs log line 2 | # 3 | # Usage: 4 | # 5 | # output_line "Cloning repository" 6 | # 7 | function output_line() { 8 | local spacing=" " 9 | echo "${spacing} $1" 10 | } 11 | 12 | # Outputs section heading 13 | # 14 | # Usage: 15 | # 16 | # output_section "Application tasks" 17 | # 18 | function output_section() { 19 | local indentation="----->" 20 | echo "${indentation} $1" 21 | } 22 | 23 | function output_warning() { 24 | local spacing=" " 25 | echo -e "${spacing} \e[31m$1\e[0m" 26 | } 27 | 28 | function output_stderr() { 29 | # Outputs to stderr in case it is inside a function so it does not 30 | # disturb the return value. Useful for debugging. 31 | echo "$@" 1>&2; 32 | } 33 | 34 | 35 | function assert_elixir_version_set() { 36 | custom_config_file=$1 37 | 38 | # 0 when found 39 | # 1 when not found 40 | # 2 when file does not exist 41 | 42 | set +e 43 | # this command is allowed to return a non-zero exit code since that is how we check if the elixir version is set. 44 | grep -q -e "^elixir_version=" $custom_config_file 2>/dev/null 45 | set -e 46 | 47 | if [ $? -ne 0 ]; then 48 | # For now, just print a warning. In the future, we will fail and require an explicit 49 | # elixir_version to be set. 50 | output_line "" 51 | output_warning "IMPORTANT: The default elixir_version will be removed on 2021-06-01. Please explicitly set an elixir_version in your elixir_buildpack.config before then or your deploys will fail." 52 | output_line "" 53 | fi 54 | } 55 | 56 | function load_config() { 57 | output_section "Checking Erlang and Elixir versions" 58 | 59 | local custom_config_file="${build_path}/elixir_buildpack.config" 60 | 61 | # Source for default versions file from buildpack first 62 | source "${build_pack_path}/elixir_buildpack.config" 63 | 64 | if [ -f $custom_config_file ]; 65 | then 66 | source $custom_config_file 67 | else 68 | output_line "Sorry, an elixir_buildpack.config is required. Please see https://github.com/HashNuke/heroku-buildpack-elixir#configuration" 69 | exit 1 70 | fi 71 | 72 | assert_elixir_version_set $custom_config_file 73 | fix_erlang_version 74 | fix_elixir_version 75 | 76 | output_line "Will use the following versions:" 77 | output_line "* Stack ${STACK}" 78 | output_line "* Erlang ${erlang_version}" 79 | output_line "* Elixir ${elixir_version[0]} ${elixir_version[1]}" 80 | } 81 | 82 | 83 | function export_env_vars() { 84 | whitelist_regex=${2:-''} 85 | blacklist_regex=${3:-'^(PATH|GIT_DIR|CPATH|CPPATH|LD_PRELOAD|LIBRARY_PATH)$'} 86 | if [ -d "$env_path" ]; then 87 | output_section "Will export the following config vars:" 88 | for e in $(ls $env_path); do 89 | echo "$e" | grep -E "$whitelist_regex" | grep -vE "$blacklist_regex" && 90 | export "$e=$(cat $env_path/$e)" 91 | : 92 | done 93 | fi 94 | } 95 | 96 | function export_mix_env() { 97 | if [ -z "$MIX_ENV" ]; then 98 | if [ -d $env_path ] && [ -f $env_path/MIX_ENV ]; then 99 | export MIX_ENV=$(cat $env_path/MIX_ENV) 100 | else 101 | export MIX_ENV=${1:-prod} 102 | fi 103 | fi 104 | 105 | output_line "* MIX_ENV=${MIX_ENV}" 106 | } 107 | 108 | function export_mix_home() { 109 | if [ -z "$MIX_HOME" ]; then 110 | if [ -d $env_path ] && [ -f $env_path/MIX_HOME ]; then 111 | export MIX_HOME=$(cat $env_path/MIX_HOME) 112 | else 113 | export MIX_HOME=$(build_mix_home_path) 114 | fi 115 | fi 116 | } 117 | 118 | function export_hex_home() { 119 | if [ -z "$HEX_HOME" ]; then 120 | if [ -d $env_path ] && [ -f $env_path/HEX_HOME ]; then 121 | export HEX_HOME=$(cat $env_path/HEX_HOME) 122 | else 123 | export HEX_HOME=$(build_hex_home_path) 124 | fi 125 | fi 126 | } 127 | 128 | function check_stack() { 129 | if [ "${STACK}" = "cedar" ]; then 130 | echo "ERROR: cedar stack is not supported, upgrade to cedar-14" 131 | exit 1 132 | fi 133 | 134 | if [ ! -f "${cache_path}/stack" ] || [ $(cat "${cache_path}/stack") != "${STACK}" ]; then 135 | output_section "Stack changed, will rebuild" 136 | $(clear_cached_files) 137 | fi 138 | 139 | echo "${STACK}" > "${cache_path}/stack" 140 | } 141 | 142 | # remove any cache files that are not under the stack-based 143 | # cache directory specified by the `stack_based_cache_path` 144 | # function 145 | function clean_old_cache_files() { 146 | rm -rf \ 147 | $(build_erlang_path) \ 148 | ${cache_path}/deps_backup \ 149 | ${cache_path}/build_backup \ 150 | ${cache_path}/.mix \ 151 | ${cache_path}/.hex 152 | rm -rf ${cache_path}/OTP-*.zip 153 | rm -rf ${cache_path}/elixir*.zip 154 | } 155 | 156 | function clean_elixir_version_dependent_cache() { 157 | rm -rf \ 158 | $(hex_backup_path) \ 159 | $(mix_backup_path) 160 | } 161 | 162 | function clean_cache() { 163 | clean_old_cache_files 164 | 165 | if [ $always_rebuild = true ]; then 166 | output_section "Cleaning all cache to force rebuilds" 167 | $(clear_cached_files) 168 | fi 169 | } 170 | 171 | function clear_cached_files() { 172 | rm -rf $(stack_based_cache_path) 173 | } 174 | 175 | function fix_erlang_version() { 176 | erlang_version=$(echo "$erlang_version" | sed 's/[^0-9.]*//g') 177 | } 178 | 179 | function fix_elixir_version() { 180 | # TODO: this breaks if there is an carriage return behind elixir_version=(branch master)^M 181 | if [ ${#elixir_version[@]} -eq 2 ] && [ ${elixir_version[0]} = "branch" ]; then 182 | force_fetch=true 183 | elixir_version=${elixir_version[1]} 184 | 185 | elif [ ${#elixir_version[@]} -eq 1 ]; then 186 | force_fetch=false 187 | 188 | # If we detect a version string (e.g. 1.14 or 1.14.0) we prefix it with "v" 189 | if [[ ${elixir_version} =~ ^[0-9]+\.[0-9]+ ]]; then 190 | # strip out any non-digit non-dot characters 191 | elixir_version=$(echo "$elixir_version" | sed 's/[^0-9.]*//g') 192 | elixir_version=v${elixir_version} 193 | fi 194 | 195 | else 196 | output_line "Invalid Elixir version specified" 197 | output_line "See the README for allowed formats at:" 198 | output_line "https://github.com/HashNuke/heroku-buildpack-elixir" 199 | exit 1 200 | fi 201 | } 202 | -------------------------------------------------------------------------------- /lib/path_funcs.sh: -------------------------------------------------------------------------------- 1 | function build_platform_tools_path() { 2 | echo "${build_path}/.platform_tools" 3 | } 4 | 5 | function runtime_platform_tools_path() { 6 | echo "${runtime_path}/.platform_tools" 7 | } 8 | 9 | function build_erlang_path() { 10 | echo "$(build_platform_tools_path)/erlang" 11 | } 12 | 13 | function runtime_erlang_path() { 14 | echo "$(runtime_platform_tools_path)/erlang" 15 | } 16 | 17 | function build_elixir_path() { 18 | echo "$(build_platform_tools_path)/elixir" 19 | } 20 | 21 | function runtime_elixir_path() { 22 | echo "$(runtime_platform_tools_path)/elixir" 23 | } 24 | 25 | function build_hex_home_path() { 26 | echo "${build_path}/.hex" 27 | } 28 | 29 | function runtime_hex_home_path() { 30 | echo "${runtime_path}/.hex" 31 | } 32 | 33 | function build_mix_home_path() { 34 | echo "${build_path}/.mix" 35 | } 36 | 37 | function runtime_mix_home_path() { 38 | echo "${runtime_path}/.mix" 39 | } 40 | 41 | function stack_based_cache_path() { 42 | echo "${cache_path}/heroku-buildpack-elixir/stack-cache" 43 | } 44 | 45 | function deps_backup_path() { 46 | echo $(stack_based_cache_path)/deps_backup 47 | } 48 | 49 | function build_backup_path() { 50 | echo $(stack_based_cache_path)/build_backup 51 | } 52 | 53 | function mix_backup_path() { 54 | echo $(stack_based_cache_path)/.mix 55 | } 56 | 57 | function hex_backup_path() { 58 | echo $(stack_based_cache_path)/.hex 59 | } 60 | 61 | function erlang_cache_path() { 62 | echo $(stack_based_cache_path)/erlang 63 | } 64 | 65 | function elixir_cache_path() { 66 | echo $(stack_based_cache_path)/elixir 67 | } 68 | -------------------------------------------------------------------------------- /test/canonical_version_test.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | . ${BUILDPACK_TEST_RUNNER_HOME}/lib/test_utils.sh 4 | . ${BUILDPACK_HOME}/lib/canonical_version.sh 5 | 6 | VERSIONS=$(fetch_erlang_versions) 7 | 8 | testExactErlangVersionAvailable() { 9 | assertEquals 0 $(exact_erlang_version_available "22.0.3" "$VERSIONS") 10 | } 11 | 12 | testExactErlangVersionUnavailable() { 13 | assertEquals 1 $(exact_erlang_version_available "22.0.1" "$VERSIONS") 14 | } 15 | 16 | testExactErlangVersionMajorOnly() { 17 | assertEquals 1 $(exact_erlang_version_available "21" "$VERSIONS") 18 | } 19 | 20 | testExactErlangVersionMajorMinorOnly() { 21 | assertEquals 0 $(exact_erlang_version_available "21.0" "$VERSIONS") 22 | } 23 | --------------------------------------------------------------------------------