├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── docs ├── Common Issues.md ├── Getting Started.md ├── Release Configuration.md ├── Upgrades and Downgrades.md ├── deployment.md └── examples.md ├── lib ├── exrm │ ├── appups.ex │ ├── config.ex │ ├── deps.ex │ ├── plugin.ex │ ├── plugins │ │ ├── appups.ex │ │ └── consolidation.ex │ └── utils │ │ ├── logger.ex │ │ └── utils.ex └── mix │ └── tasks │ ├── release.clean.ex │ ├── release.ex │ └── release.plugins.ex ├── mix.exs ├── mix.lock ├── priv └── rel │ ├── files │ ├── boot │ ├── boot.bat │ ├── boot_shim │ ├── boot_shim.bat │ ├── install_upgrade.escript │ ├── nodetool │ ├── release_definition.txt │ ├── sys.config │ └── vm.args │ └── relx.config └── test ├── appups_test.exs ├── fixtures ├── beams │ ├── v1 │ │ └── ebin │ │ │ ├── Elixir.Test.Server.beam │ │ │ ├── Elixir.Test.Supervisor.beam │ │ │ └── Elixir.Test.beam │ └── v2 │ │ └── ebin │ │ ├── Elixir.Asd.beam │ │ ├── Elixir.Test.Server.beam │ │ ├── Elixir.Test.Supervisor.beam │ │ ├── Elixir.Test.beam │ │ └── test.appup ├── configs │ ├── merged_relx.config │ ├── new_relx.config │ └── old_relx.config ├── example_app │ ├── .gitignore │ ├── config │ │ ├── config.all.exs │ │ ├── config.dev.exs │ │ ├── config.exs │ │ ├── config.prod.exs │ │ ├── config.test.exs │ │ ├── explicit_config.exs │ │ ├── test.conf │ │ └── test.schema.exs │ ├── lib │ │ ├── test.ex │ │ └── test │ │ │ ├── server.ex │ │ │ └── supervisor.ex │ ├── mix.exs │ ├── priv │ │ └── sample.txt │ └── relx.config └── fake_project │ ├── .gitignore │ ├── lib │ └── fake_project.ex │ └── mix.exs ├── plugin_test.exs ├── test_helper.exs └── utils_test.exs /.gitignore: -------------------------------------------------------------------------------- 1 | /_build 2 | /deps 3 | /tmp 4 | /docs/build 5 | erl_crash.dump 6 | *.ez 7 | .exenv-version 8 | /doc 9 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: elixir 3 | elixir: 4 | - 1.1.0 5 | - 1.2.0 6 | otp_release: 7 | - 18.1 8 | env: 9 | - MIX_ENV=test 10 | script: 11 | - mix test --exclude expensive 12 | - mix test --only expensive 13 | notifications: 14 | email: 15 | - paulschoenfelder@gmail.com 16 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to this project will be documented in this file (at least to the extent possible, I am not infallible sadly). 4 | This project adheres to [Semantic Versioning](http://semver.org/). 5 | 6 | ## [Unreleased] 7 | ### Added 8 | - N/A 9 | ### Changed 10 | - N/A 11 | ### Deprecated 12 | - N/A 13 | ### Removed 14 | - N/A 15 | ### Fixed 16 | - N/A 17 | 18 | ## 1.0.2 19 | ### Fixed 20 | - Logger implementation 21 | 22 | ## 1.0.1 23 | ### Fixed 24 | - Usage of `readlink -f` is invalid on OSX. #306 25 | 26 | ## 1.0.0 27 | ### Added 28 | - Added CHANGELOG 29 | - Added `command` subcommand to boot script. Use with `bin/app command [arg1...]` 30 | - Updated relx to latest release (3.18.0) 31 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing To Exrm 2 | 3 | I've thrown together the guidelines around how to contribute exrm so it's smooth 4 | sailing for everyone. Please take some time and read through this before creating 5 | a pull request. Your contributions are hugely important to the success of exrm, 6 | and I appreciate all your help! 7 | 8 | - [Issues Tracker](#issues-tracker) 9 | - [Bug Reports](#bug-reports) 10 | - [Feature Requests](#feature-requests) 11 | - [Contributing](#contributing) 12 | - [Pull Requests](#pull-requests) 13 | 14 | ## Issues Tracker 15 | 16 | I use the issues tracker to do the following things: 17 | 18 | * **requests for consideration (RFC)** - These are things which I, or you perhaps, am 19 | soliciting feedback on, in order to flesh out ideas, potential features, or big 20 | changes. If you have an idea, or a feature you'd like to implement, feel free to 21 | create issues that fit that definition, and I'll give them the RFC label. 22 | * **[bug reports](#bug-reports)** - Anything you encounter with exrm that is broken or 23 | is generally bad behavior, create an issue for it, and I'll label it appropriately. 24 | * **[submitting pull requests](#pull-requests)** - If you found and fixed a bug in exrm, 25 | please submit a PR with your changes! See the link for guidelines on PRs. 26 | 27 | All bug reports are given a difficulty classification between `starter` and `advanced`. 28 | This is so anyone can hop in and start pulling bugs off the stack if they wish to get 29 | involved. If you are new to the project, please start with one of the `starter` or 30 | `intermediate` bugs, as most of the stuff classified as `advanced` require intimate 31 | knowledge of exrm's internals. Regardless of level, if it's something you feel you want 32 | to tackle, leave a comment and let me know, and we can discuss it in more detail. 33 | 34 | ## Bug Reports 35 | 36 | A bug is a _demonstrable problem_ that is caused by the code in the repository. 37 | 38 | Guidelines for bug reports: 39 | 40 | 1. **Use the GitHub issue search** — check if the issue has already been 41 | reported. 42 | 43 | 2. **Check if the issue has been fixed** — try to reproduce it using the 44 | `master` branch in the repository. 45 | 46 | 3. **Isolate and report the problem** — ideally create a reduced test 47 | case. There are two test projects that are good starting points for this, 48 | [exrm-test](https://github.com/bitwalker/exrm-test) and 49 | [exrm-umbrella-test](https://github.com/bitwalker/exrm-umbrella-test) 50 | 51 | Please try to be as detailed as possible in your report. Include information about 52 | your operating system, your Erlang and Elixir versions (i.e. 17.1.2, or 0.14.3). 53 | Provide steps to reproduce the issue as well as the outcome you were expecting. All 54 | these details will help other developers to find and fix the bug. 55 | 56 | Example: 57 | 58 | > Short and descriptive example bug report title 59 | > 60 | > A summary of the issue and the environment in which it occurs. If suitable, 61 | > include the steps required to reproduce the bug. 62 | > 63 | > 1. This is the first step 64 | > 2. This is the second step 65 | > 3. Further steps, etc. 66 | > 67 | > `` - a link to the reduced test case (e.g. a GitHub Gist or project repo) 68 | > 69 | > Any other information you want to share that is relevant to the issue being 70 | > reported. This might include the lines of code that you have identified as 71 | > causing the bug, and potential solutions (and your opinions on their 72 | > merits). 73 | 74 | ## Feature Requests 75 | 76 | Feature requests are absolutely welcome, but before you dive in to implementing 77 | an idea, please open up an issue on the tracker as a request for consideration by 78 | creating the title of your issue prefixed with RFC. 79 | 80 | Example: 81 | 82 | > RFC: Some feature that would be super awesome 83 | > 84 | > A description of the new feature and why it's needed. This should open 85 | > up discussion and provide a starting point for other participants 86 | > to give their thoughts on whether the feature makes sense, what the best 87 | > path to implementation is, etc. If you made code changes to validate your 88 | > idea, link the url so others can look at the work you've done. 89 | 90 | Feature requests will be discussed by users of exrm, and the final vote will be made 91 | by me on whether or not it fits within the goals of the project. If there is 92 | strong merit for a feature to be implemented, you can be assured I will be interested 93 | in making it happen. 94 | 95 | ## Contributing 96 | 97 | Exrm is divided into a few major components within the `lib` folder: 98 | 99 | - `mix/tasks/release.ex` - This defines the primary release generation task for mix 100 | - `mix/tasks/release.clean.ex` - This defines the cleanup task for mix 101 | - `mix/tasks/release.plugins.ex` - This task allows users to see what active exrm plugins are loaded 102 | - `exrm/plugins/conform.ex` - This exrm plugin handles generating a `sys.config` for a release using conform 103 | - `exrm/plugins/consolidation.ex` - This exrm plugin handles performing protocol consolidation for a release 104 | - `exrm/appups.ex` - This module handles generating `.appup` files for release upgrades. 105 | - `exrm/config.ex` - This module contains the configuration struct containing the state and configuration of the release. 106 | - `exrm/plugin.ex` - This module defines the plugin behavior used by exrm plugins 107 | - `exrm/utils.ex` - This module provides core utilities for exrm 108 | 109 | After your changes are done, please remember to run the full test suite with 110 | `mix test`. 111 | 112 | With tests running and passing, and your [documentation](#contributing-documentation) done, your ready to send a PR! 113 | 114 | If you decide you want to build the html docs as well, you'll need the following in order to use the `mix docs` task: 115 | 116 | - python 117 | - node 118 | - bower and grunt: `npm install -g bower grunt` 119 | - sphinx: `pip install Sphinx` 120 | 121 | Then you should be able to run `mix docs` or `mix docs watch` to build and/or watch the html docs during development. 122 | 123 | ## Contributing Documentation 124 | 125 | Please make sure all modules are well documented with a `@moduledoc`, any relevant 126 | `@typedoc`s and all public functions documented with `@doc` and `@spec`. Use examples 127 | where possible (especially in doctest format if it's possible). There may be legacy 128 | code still in there without these, so if you see them, feel free to make a pull request 129 | to add more docs! 130 | 131 | Example: 132 | 133 | ```elixir 134 | @doc """ 135 | Return only those elements for which `fun` is true. 136 | 137 | ## Examples 138 | 139 | iex> Enum.filter([1, 2, 3], fn(x) -> rem(x, 2) == 0 end) 140 | [2] 141 | 142 | """ 143 | def filter(collection, fun) ... 144 | ``` 145 | 146 | ## Pull requests 147 | 148 | Good pull requests - patches, improvements, new features - are a fantastic 149 | help. They should remain focused in scope and avoid containing unrelated 150 | commits. 151 | 152 | **IMPORTANT**: By submitting a patch, you agree that your work will be 153 | licensed under the license used by the project. 154 | 155 | If you have any large pull request in mind (e.g. implementing features, 156 | refactoring code, etc), **please ask first** otherwise you risk spending 157 | a lot of time working on something that the project's developers might 158 | not want to merge into the project. 159 | 160 | Please adhere to the coding conventions in the project (indentation, 161 | accurate comments, etc.) and don't forget to add your own tests and 162 | documentation. When working with git, we recommend the following process 163 | in order to craft an excellent pull request: 164 | 165 | 1. [Fork](http://help.github.com/fork-a-repo/) the project, clone your fork, 166 | and configure the remotes: 167 | 168 | ```bash 169 | # Clone your fork of the repo into the current directory 170 | git clone https://github.com//exrm 171 | # Navigate to the newly cloned directory 172 | cd exrm 173 | # Assign the original repo to a remote called "upstream" 174 | git remote add upstream https://github.com/bitwalker/exrm 175 | ``` 176 | 177 | 2. If you cloned a while ago, get the latest changes from upstream: 178 | 179 | ```bash 180 | git checkout master 181 | git pull upstream master 182 | ``` 183 | 184 | 3. Create a new topic branch (off of `master`) to contain your feature, change, 185 | or fix. 186 | 187 | **IMPORTANT**: Making changes in `master` is discouraged. You should always 188 | keep your local `master` in sync with upstream `master` and make your 189 | changes in topic branches. 190 | 191 | ```bash 192 | git checkout -b 193 | ``` 194 | 195 | 4. Commit your changes in logical chunks. Keep your commit messages organized, 196 | with a short description in the first line and more detailed information on 197 | the following lines. Feel free to use Git's 198 | [interactive rebase](https://help.github.com/articles/interactive-rebase) 199 | feature to tidy up your commits before making them public. 200 | 201 | 5. Make sure all the tests are still passing. 202 | 203 | ```bash 204 | mix test 205 | ``` 206 | 207 | 6. Push your topic branch up to your fork: 208 | 209 | ```bash 210 | git push origin 211 | ``` 212 | 213 | 7. [Open a Pull Request](https://help.github.com/articles/using-pull-requests/) 214 | with a clear title and description. 215 | 216 | 8. If you haven't updated your pull request for a while, you should consider 217 | rebasing on master and resolving any conflicts. 218 | 219 | **IMPORTANT**: _Never ever_ merge upstream `master` into your branches. You 220 | should always `git rebase` on `master` to bring your changes up to date when 221 | necessary. 222 | 223 | ```bash 224 | git checkout master 225 | git pull upstream master 226 | git checkout 227 | git rebase master 228 | ``` 229 | 230 | Thank you for your contributions! 231 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Paul Schoenfelder 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all 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, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Elixir Release Manager 2 | 3 | [![Hex.pm Version](http://img.shields.io/hexpm/v/exrm.svg?style=flat)](https://hex.pm/packages/exrm) 4 | 5 | The full documentation for Exrm is located [here](http://hexdocs.pm/exrm). 6 | 7 | Thanks to @tylerflint for the original Makefile, rel.config, and runner script 8 | which inspired this project! 9 | 10 | **IMPORTANT: This project has been replaced by [Distillery](https://github.com/bitwalker/distillery)**. 11 | 12 | I am no longer maintaining this project - Distillery fully replaces it in every way, but does 13 | require that you are on Elixir 1.3+. 14 | 15 | ## License 16 | 17 | This project is MIT licensed. Please see the `LICENSE.md` file for more details. 18 | -------------------------------------------------------------------------------- /docs/Common Issues.md: -------------------------------------------------------------------------------- 1 | # Common Issues 2 | 3 | ## Problems often encountered by new users 4 | 5 | I'm starting this list to begin collating the various caveats around 6 | building releases. As soon as I feel like I have a firm grasp of all the 7 | edge cases, I'll formalize this in a better format perhaps as a 8 | "Preparing for Release" document. 9 | 10 | 11 | ## Dependency issues 12 | 13 | Ensure all dependencies for your application are defined in either the 14 | `:applications` or `:included_applications` block of your `mix.exs` file. This is how the build 15 | process knows that those dependencies need to be bundled in to the 16 | release. **This includes dependencies of your dependencies, if they were 17 | not properly configured**. For instance, if you depend on `mongoex`, and 18 | `mongoex` depends on `erlang-mongodb`, but `mongoex` doesn't have `erlang-mongodb` 19 | in it's applications list, your app will fail in it's release form, 20 | because `erlang-mongodb` won't be loaded. 21 | 22 | If you are running into issues with your dependencies missing their 23 | dependencies, it's likely that the author did not put the dependencies in 24 | the `:application` block of *their* `mix.exs`. You may have to fork, or 25 | issue a pull request in order to resolve this issue. Alternatively, if 26 | you know what the dependency is, you can put it in your own `mix.exs`, and 27 | the release process will ensure that it is loaded with everything else. 28 | 29 | 30 | ## Configuration not working as expected 31 | 32 | Due to the way `config.exs` is converted to the `sys.config` file used by 33 | Erlang releases, it is important to make sure all of your config values are 34 | namespaced by application, i.e. `config :myapp, foo: bar` instead of `config foo: bar`, 35 | and access your config via `Application.get_env(:myapp, :foo)`. If you do not 36 | do this, you will likely run into issues at runtime complaining that you are attempting 37 | to access configuration for an application that is not loaded. 38 | 39 | ## Packaging fails with errors related to `erl_tar` 40 | 41 | If your project has files or modules names which exceed the file name length limit of `erl_tar`, 42 | you will see an error like the following: 43 | 44 | ``` 45 | Building release with MIX_ENV=dev. 46 | {{case_clause, 47 | {'EXIT', 48 | {function_clause, 49 | [{filename,join,[[]],[{file,"filename.erl"},{line,393}]}, 50 | {erl_tar,split_filename,4,[{file,"erl_tar.erl"},{line,471}]}, 51 | {erl_tar,create_header,3,[{file,"erl_tar.erl"},{line,400}]}, 52 | {erl_tar,add1,4,[{file,"erl_tar.erl"},{line,323}]}, 53 | {systools_make,add_to_tar,3, 54 | [{file,"systools_make.erl"},{line,1879}]}, 55 | {lists,foreach,2,[{file,"lists.erl"},{line,1337}]}, 56 | {systools_make,'-add_applications/5-fun-0-',6, 57 | [{file,"systools_make.erl"},{line,1569}]}, 58 | {lists,foldl,3,[{file,"lists.erl"},{line,1262}]}]}}}, 59 | [{systools_make,'-add_applications/5-fun-0-',6, 60 | [{file,"systools_make.erl"},{line,1569}]}, 61 | {lists,foldl,3,[{file,"lists.erl"},{line,1262}]}, 62 | {systools_make,add_applications,5,[{file,"systools_make.erl"},{line,1568}]}, 63 | {systools_make,mk_tar,6,[{file,"systools_make.erl"},{line,1562}]}, 64 | {systools_make,mk_tar,5,[{file,"systools_make.erl"},{line,1538}]}, 65 | {systools_make,make_tar,2,[{file,"systools_make.erl"},{line,336}]}, 66 | {rlx_prv_archive,make_tar,3,[{file,"src/rlx_prv_archive.erl"},{line,83}]}, 67 | {relx,run_provider,2,[{file,"src/relx.erl"},{line,308}]}]} 68 | ==> ERROR: "Failed to build release. Please fix any errors and try again." 69 | ``` 70 | 71 | ## Release not starting correctly due to Joken version < 1.2.0 72 | 73 | Joken < 1.2.0 causes a deadlock during application load, this affects `start`, `console` 74 | and other commands. 75 | 76 | ## Release not starting on Vagrant's `/vagrant` mountpoint 77 | 78 | When running in Vagrant with source and release dirs under the `/vagrant` directory, you might eed to set RELEASE_MUTABLE_DIR envar to a local path that is not under `/vagrant` 79 | 80 | ## Release not starting for other reasons - diagnosis 81 | 82 | exrm 1.0.4 and later - set `ERL_OPTS="-init_debug"` envvar when running your app. 83 | You can tweak the `myapp.sh` script found inside the versioned directory. 84 | 85 | For older versions, edit the startup script (`rel/myapp/releases/1.0.0/myapp.sh`) and edit the ERL_OPTS line to say `ERL_OPTS="-init_debug"`. 86 | 87 | ## Others 88 | 89 | If you run into problems, please create an issue, and I'll address ASAP. 90 | -------------------------------------------------------------------------------- /docs/Getting Started.md: -------------------------------------------------------------------------------- 1 | # Getting Started 2 | 3 | ## How to get up and running with releases 4 | 5 | This project's goal is to make releases with Elixir projects a breeze. It is composed of a mix task, and build files required to successfully take your Elixir project and perform a release build, and a [simplified configuration mechanism](https://github.com/bitwalker/conform) which integrates with your current configuration and makes it easy for your operations group to configure the release once deployed. All you have to do to get started is the following: 6 | 7 | Start by adding exrm as a dependency to your project: 8 | 9 | ```elixir 10 | defp deps do 11 | [{:exrm, "~> 0.18.1"}] 12 | end 13 | ``` 14 | 15 | ### Usage 16 | 17 | You can build a release with the `release` task: 18 | 19 | ``` 20 | $ mix release 21 | ``` 22 | 23 | This task constructs the complete release for you. The output is sent to `rel/`. To see what flags you can pass to this task, use `mix help release`. 24 | 25 | You can start a console connected to the release build of your application with: 26 | 27 | ``` 28 | $ rel//bin/ console 29 | ``` 30 | 31 | ### Testing a release during development 32 | 33 | Rather than having to build a release, deploy, then test, you can actually test your release during development by using `mix release --dev`. 34 | 35 | This symlinks your application's code into the release, allowing you to make code changes, then recompile and restart your release to see the changes. Being able to rapidly test and tweak your release like this goes a long way to making the release process less tedious! 36 | 37 | ### Cleanup 38 | 39 | You can clean up release artifacts produced by exrm with: 40 | 41 | ``` 42 | $ mix release.clean 43 | ``` 44 | 45 | This will clean up any temporary artifacts related to the current version, and allow you to effectively start a release build from scratch. 46 | 47 | By passing the `--implode` flag, you can further extend the clean up to *all* release related artifacts, effectively resetting yourself to a pre-exrm state. This should be done carefully, as anything related to releases will be removed! 48 | 49 | You can pass the `--no-confirm` flag in addition to `--implode` if you want to bypass exrm's warning about removing all artifacts (this is primarily for automated tasks, but might come in useful during testing scenarios) 50 | 51 | ### **IMPORTANT** 52 | 53 | It is currently not supported to perform hot upgrades/downgrades from the `rel` directory. This is because the upgrade/downgrade process deletes files from the release when it is installed, which will cause issues when you are attempting to build a release of the next version of your app. It is important that you do actual deployments of your app outside of the build directory! 54 | -------------------------------------------------------------------------------- /docs/Release Configuration.md: -------------------------------------------------------------------------------- 1 | # Release Configuration 2 | 3 | ## How to configure your release 4 | 5 | There are two forms of configuration I will deal with here. One is 6 | configuration for the release process itself, the latter is handling 7 | application configuration for your release. The following custom release 8 | configuration is supported: 9 | 10 | - `rel/sys.config` - This is the configuration file the release will use in production. I would use `config/config.exs` or `config/myapp.conf` (if using conform) instead of this, but it's there if you want it. 11 | - `rel/vm.args` - This file contains line-separated arguments that the Erlang VM will use when booting up. Provide your own here and it will be used instead of the default one. Make sure you provide values for `sname` and `cookie` though, or you won't be able to connect to your release! 12 | - `rel/relx.config` - This file is used to provide configuration to exrm's underyling relx dependency. See the documentation at [relx's GitHub page](https://github.com/erlware/relx) for more information on what you can provide here. The default one should cover 99% of cases, but if you need to tweak values, you can provide your own relx configuration, and setting the config values you care about. You do not need to provide the entire configuration, as your customizations will be merged with the defaults exrm uses. 13 | 14 | Elixir has support for providing configuration using Elixir terms in a 15 | `config/config.exs` file. While this is perfectly usable, it's not very 16 | simple for your operations group to work with, and generally contains no 17 | useful documentation on what each setting is for or what they do. To 18 | help make configuration much more easy and maintainable, exrm bundles a 19 | dependency called [conform](https://github.com/bitwalker/conform). It is optional to use, but is there if you desire to use it. 20 | 21 | ### Using Conform with Exrm 22 | 23 | Conform relies primarily on two files: a `.schema.exs` file, and 24 | a `.conf` file. The .conf file is where you will configure your 25 | app, and the .schema.exs file is where you define what configuration is 26 | available in the .conf, and how it is translated to the final 27 | `sys.config` that your release loads up at runtime. 28 | 29 | Conform itself has the best documentation on how to work with these files, 30 | and to see an example app which makes use of this, check out the 31 | [exrm-test project](https://github.com/bitwalker/exrm-test). 32 | 33 | Here's a quick rundown on how it works. You probably already have a `config.exs` file, and if 34 | you don't that's fine, it's not required. If you do have one already, 35 | you can compile your project and run `mix conform.new` to generate the 36 | conform schema from your current configuration. If you don't have one, 37 | check out the conform README on how to create one. Once you have the 38 | schema file in your `config` directory, you can work off the 39 | definitions generated from your current config, and/or start adding 40 | definitions for config settings you wish to add. 41 | 42 | Once your schema is all set, you can generate the default .conf file for 43 | your app using `mix conform.configure`. This will output a .conf file to 44 | `config/yourapp.conf`. This will be bundled with your release, and 45 | located in `$DEPLOY_DIR/releases/$RELEASE_VER/myapp.conf` per default 46 | (also it could be moved, using `RELEASE_CONFIG_FILE` or `RELEASE_CONFIG_DIR` environment variables). 47 | Your ops group can then do all their configuration in production via that file. 48 | 49 | If you are wondering how that .conf file is usable by the VM, it's very 50 | simple. When you run `/bin/ start`, or any other command which boots 51 | your app, a conform escript is run which translates the .conf via the 52 | schema (also bundled with the release) to Elixir terms, that is then 53 | merged over the top of the sys.config which is also bundled with the 54 | release, and then saved over the top of the existing sys.config. Once 55 | the escript has finished executing, your app is booted using that 56 | sys.config file, and everything carries on like normal. 57 | 58 | NOTE: Your `config/config.exs` file is still converted to the 59 | `sys.config` which is bundled with the release. If you wish to hide 60 | settings from your end users, put them in there, and remove the 61 | definitions for them from your schema file. The `sys.config` is merged 62 | with the configuration which is defined in the .conf, so your settings 63 | will still be applied, they just won't be exposed for end users. 64 | 65 | You can also change the directory of all your configuration files `sys.config`, 66 | `vm.args` and `.conf` using `RELEASE_CONFIG_DIR` or only for conform 67 | config `.conf` using `RELEASE_CONFIG_FILE` system environments like this: 68 | 69 | `RELEASE_CONFIG_DIR=/some_path_to_configs bin/ start` 70 | 71 | or 72 | 73 | `RELEASE_CONFIG_FILE=/some_path_to_configs/.conf bin/ start` 74 | 75 | So you can have persistent configuration for your application. 76 | 77 | The configs placed in `$DEPLOY_DIR/releases/$RELEASE_VER` will be used 78 | as persistent default configs. They will be used by first release start and placed in 79 | `$DEPLOY_DIR/releases/$RELEASE_VER/running-config` if no 80 | `RELEASE_MUTABLE_DIR` defined. It is also possible to move the running-config, logs 81 | and erl_pipes using `RELEASE_MUTABLE_DIR` system environment. The idea is to 82 | hold persistent and non-persistent data separately. 83 | 84 | **NOTE**: If not using conform, and relying on `config.exs`, you cannot use dynamic code which relies on the runtime environment, i.e: 85 | 86 | ``` 87 | config :myapp, 88 | foo: System.get_env("FOOBAR") 89 | ``` 90 | 91 | The reason for this is that the Erlang VM uses `sys.config` for configuration, and `sys.config` can only contain static terms, not function calls or other dynamic code. When your `config.exs` is evaluated and converted to `sys.config`, the dynamic code in `config.exs` is executed, evaluated, and the result is persisted in `sys.config`. If you are relying on such things as environment variables in `config.exs`, the value stored in `sys.config` will be the value of those variables when the build was produced, not their values when the release is booted, which is almost certainly not what you intended. When running your app with `iex -S mix` or `mix run --no-halt`, the way configuration is evaluated is different, as Mix will load the config from `config.exs`, and overwrite whatever is in the default configuration. As neither Mix, nor your `config.exs` is present in a release, this is not possible. If you need to load configuration from the environment at runtime, you will need to do something like the following: 92 | 93 | ``` 94 | my_setting = Application.get_env(:myapp, :setting) || System.get_env("MY_SETTING") || default_val 95 | ``` 96 | -------------------------------------------------------------------------------- /docs/Upgrades and Downgrades.md: -------------------------------------------------------------------------------- 1 | # Upgrades/Downgrades 2 | 3 | ## How to perform hot upgrades and downgrades! 4 | 5 | Note: This documentation assumes you've done an initial deployment to `/tmp` per the Deployment docs. I would suggest starting there just to make sure you understand the prerequisites. 6 | 7 | **Important**: In order to build upgradable releases, you need to have the previous release available in `rel`. Without it, the appup script can not be generated. 8 | There are various approaches to storing the contents of `rel` (git, use a single build server, S3, etc.), but the important part is that you pick one. 9 | When you are about to build a new release, make sure the previous release is available to the build (under `rel`), and you'll be good to go! 10 | 11 | So you've made some changes to your app, and you want to generate a new release and perform a hot upgrade. I'm here to tell you that this is going to be a breeze, so I hope you're ready (I'm using my test app as an example here again): 12 | 13 | 1. `mix release` 14 | 2. `mkdir -p /tmp/test/releases/0.0.2` 15 | 3. `cp rel/test/releases/0.0.2/test.tar.gz /tmp/test/releases/0.0.2/` 16 | 4. `cd /tmp/test` 17 | 5. `bin/test upgrade "0.0.2"` 18 | 19 | Annnnd we're done. Your app was upgraded in place with no downtime, and is now running your modified code. You can use `bin/test remote_console` to connect and test to be sure your changes worked as expected. 20 | 21 | You can also provide your own .appup file, by writing one and placing it in 22 | `rel/.appup`. This location is checked before generating a new 23 | release, and will be used instead of autogenerating an appup file for 24 | you. If you don't know what an appup file is, it is effectively the file which describes how the upgrade will be performed. To learn more about what goes in this file and how appups work, please consult the Erlang documentation for appups, which is located [here](http://www.erlang.org/doc/design_principles/appup_cookbook.html). 25 | 26 | ## Downgrading Releases 27 | 28 | This is even easier! Using the example from before: 29 | 30 | 1. `cd /tmp/test` 31 | 2. `bin/test downgrade "0.0.1"` 32 | 33 | All done! 34 | -------------------------------------------------------------------------------- /docs/deployment.md: -------------------------------------------------------------------------------- 1 | # Deployment 2 | 3 | ## How to deploy your release 4 | 5 | A quick word of warning: It is currently not supported to perform hot upgrades/downgrades from the `rel` directory. This is because the upgrade/downgrade process deletes files from the release when it is installed, which will cause issues when you are attempting to build a release of the next version of your app. It is important that you do actual deployments of your app outside of the build directory if you plan on using this feature of releases! 6 | 7 | First lets talk about how you can run your release after executing `mix release`. The following example code is based on the [exrm-test project](https://github.com/bitwalker/exrm-test): 8 | 9 | ``` 10 | > rel/test/bin/test console 11 | Erlang/OTP 17 [erts-6.0] [source] [64-bit] [smp:4:4] [ds:4:4:10] [async-threads:10] [hipe] [kernel-poll:false] [dtrace] 12 | 13 | Interactive Elixir (1.0.5) - press Ctrl+C to exit (type h() ENTER for help) 14 | iex(test@127.0.0.1)1> :gen_server.call(:test, :ping) 15 | :v1 16 | iex(test@127.0.0.1)2> 17 | ``` 18 | 19 | As you can see from the above example, running the `console` command allows you to boot your release with an `iex` console just like if you had run `iex -S mix`. This allows you to quickly test and play around with your running release build! 20 | 21 | 22 | ### Deployment 23 | 24 | Now that you've generated your first release, it's time to deploy it! Let's walk through a simulated deployment to the `/tmp` directory on your machine: 25 | 26 | 1. `mix release` 27 | 2. `mkdir -p /tmp/test` 28 | 3. `cp rel/test/releases/0.0.1/test.tar.gz /tmp/` 29 | 4. `cd /tmp/test` 30 | 5. `tar -xf /tmp/test.tar.gz` 31 | 32 | Now to start your app: 33 | 34 | ``` 35 | $ bin/test start 36 | ``` 37 | 38 | You can test if your app is alive and running with: 39 | 40 | ``` 41 | $ bin/test ping 42 | ``` 43 | 44 | If you want to connect a remote shell to your now running app: 45 | 46 | ``` 47 | $ bin/test remote_console 48 | ``` 49 | 50 | Ok, you should be staring at a standard `iex` prompt, but slightly different, something like: 51 | 52 | ``` 53 | iex(test@localhost)1> 54 | ``` 55 | 56 | The prompt shows us that we are currently connected to `test@localhost`, which is the value of `name` in our `vm.args` file. Feel free to ping the app using `:gen_server.call(:test, :ping)` to make sure it works (just to recap, this is based on the example app described above, your own application will not have this function available). 57 | 58 | At this point, you can't just abort from the prompt like usual and make the node shut down (which is what occurs when you are doing this from the `console` command). This would be an obviously bad thing in a production environment. Instead, you can execute `:init.stop` from the `iex` prompt, and this will shut down the node. You will still be connected to the shell, but once you quit the shell, the node is gone. 59 | 60 | ### Executing code against a running release 61 | 62 | If you want to execute a command against your running node without 63 | attaching a shell you can do something like the following: 64 | 65 | ``` 66 | $ bin/test rpc erlang now 67 | ``` 68 | 69 | or 70 | 71 | ``` 72 | $ bin/test rpc calendar valid_date "{2014,3,14}." 73 | ``` 74 | 75 | Notice that the arguments required are in module, function, argument 76 | format. The argument parameter will be evaluated as an Erlang term, 77 | and applied to the module/function. Multiple args should be formatted as 78 | a list, i.e. `[arg1, arg2, arg3].`. 79 | -------------------------------------------------------------------------------- /docs/examples.md: -------------------------------------------------------------------------------- 1 | # Examples 2 | 3 | ## Example applications for your reference 4 | 5 | You can find the source code for an example application [here](https://github.com/bitwalker/exrm-test), and an example umbrella application [here](https://github.com/bitwalker/exrm-umbrella-test). Everything mentioned here should work out of the box with those projects. If it does not, please file a bug! 6 | -------------------------------------------------------------------------------- /lib/exrm/appups.ex: -------------------------------------------------------------------------------- 1 | defmodule ReleaseManager.Appups do 2 | @moduledoc """ 3 | Module for auto-generating appups between releases. 4 | """ 5 | import ReleaseManager.Utils, only: [write_term: 2] 6 | 7 | @doc """ 8 | Generate a .appup for the given application, start version, and upgrade version. 9 | 10 | ## Parameter information 11 | application: the application name as an atom 12 | v1: the start version, such as "0.0.1" 13 | v2: the upgrade version, such as "0.0.2" 14 | v1_path: the path to the v1 artifacts (rel//lib/-0.0.1) 15 | v2_path: the path to the v2 artifacts (_build/prod/lib/) 16 | 17 | """ 18 | def make(application, v1, v2, v1_path, v2_path) do 19 | v1_release = 20 | v1_path 21 | |> Path.join("/ebin/") 22 | |> Path.join(Atom.to_string(application) <> ".app") 23 | |> String.to_char_list 24 | v2_release = 25 | v2_path 26 | |> Path.join("/ebin/") 27 | |> Path.join(Atom.to_string(application) <> ".app") 28 | |> String.to_char_list 29 | 30 | case :file.consult(v1_release) do 31 | { :ok, [ { :application, ^application, v1_props } ] } -> 32 | case vsn(v1_props) === v1 do 33 | true -> 34 | case :file.consult(v2_release) do 35 | { :ok, [ { :application, ^application, v2_props } ] } -> 36 | case vsn(v2_props) === v2 do 37 | true -> 38 | make_appup(application, v1, v1_path, v1_props, v2, v2_path, v2_props) 39 | false -> 40 | { :error, :bad_new_appvsn } 41 | end 42 | _ -> 43 | { :error, :bad_new_appfile } 44 | end 45 | false -> 46 | { :error, :bad_old_appvsn } 47 | end 48 | _ -> 49 | { :error, :bad_old_appfile } 50 | end 51 | end 52 | 53 | defp make_appup(application, v1, v1_path, _v1_props, v2, v2_path, _v2_props) do 54 | {only_v1, only_v2, different} = 55 | :beam_lib.cmp_dirs(to_char_list(Path.join(v1_path, "ebin")), to_char_list(Path.join(v2_path, "ebin"))) 56 | 57 | appup = 58 | { v2 |> String.to_char_list, 59 | [ { v1 |> String.to_char_list, 60 | (for file <- only_v2, do: generate_instruction(:added, file)) ++ 61 | (for {v1_file, v2_file} <- different, do: generate_instruction(:changed, {v1_file, v2_file})) ++ 62 | (for file <- only_v1, do: generate_instruction(:deleted, file)) 63 | } 64 | ], 65 | [ { v1 |> String.to_char_list, 66 | (for file <- only_v2, do: generate_instruction(:deleted, file)) ++ 67 | (for {v1_file, v2_file} <- different, do: generate_instruction(:changed, {v1_file, v2_file})) ++ 68 | (for file <- only_v1, do: generate_instruction(:added, file)) 69 | } 70 | ] 71 | } 72 | 73 | # Save the appup to the upgrade's build directory 74 | v2_path 75 | |> Path.join("ebin") 76 | |> Path.join((application |> Atom.to_string) <> ".appup") 77 | |> write_term(appup) 78 | 79 | { :ok, appup } 80 | end 81 | 82 | defp generate_instruction(:added, file), do: {:add_module, module_name(file)} 83 | defp generate_instruction(:deleted, file), do: {:delete_module, module_name(file)} 84 | defp generate_instruction(:changed, {v1_file, _v2_file}) do 85 | module_name = module_name(v1_file) 86 | attributes = beam_attributes(v1_file) 87 | exports = beam_exports(v1_file) 88 | is_supervisor = is_supervisor?(attributes) 89 | is_special_proc = is_special_process?(exports) 90 | generate_instruction_advanced(module_name, is_supervisor, is_special_proc) 91 | end 92 | 93 | defp beam_attributes(file) do 94 | {:ok, {_, [attributes: attributes]}} = :beam_lib.chunks(file, [:attributes]) 95 | attributes 96 | end 97 | 98 | defp beam_exports(file) do 99 | {:ok, {_, [exports: exports]}} = :beam_lib.chunks(file, [:exports]) 100 | exports 101 | end 102 | 103 | defp is_special_process?(exports) do 104 | Keyword.get(exports, :system_code_change) == 4 || 105 | Keyword.get(exports, :code_change) == 3 106 | end 107 | 108 | defp is_supervisor?(attributes) do 109 | behaviours = Keyword.get(attributes, :behavior, []) ++ 110 | Keyword.get(attributes, :behaviour, []) 111 | (:supervisor in behaviours) || (Supervisor in behaviours) 112 | end 113 | 114 | # supervisor 115 | defp generate_instruction_advanced(m, true, _is_special), do: {:update, m, :supervisor} 116 | # special process (i.e. exports code_change/3 or system_code_change/4) 117 | defp generate_instruction_advanced(m, _is_sup, true), do: {:update, m, {:advanced, []}} 118 | # non-special process (i.e. neither code_change/3 nor system_code_change/4 are exported) 119 | defp generate_instruction_advanced(m, _is_sup, false), do: {:load_module, m} 120 | 121 | defp module_name(file) do 122 | :beam_lib.info(file) |> Keyword.fetch!(:module) 123 | end 124 | 125 | defp vsn(props) do 126 | { :value, { :vsn, vsn } } = :lists.keysearch(:vsn, 1, props) 127 | vsn |> List.to_string 128 | end 129 | 130 | end 131 | -------------------------------------------------------------------------------- /lib/exrm/config.ex: -------------------------------------------------------------------------------- 1 | defmodule ReleaseManager.Config do 2 | @moduledoc """ 3 | Configuration for the release task. 4 | 5 | Contains the following values: 6 | 7 | name: The name of your application 8 | version: The version of your application 9 | dev?: Is this release being built in dev mode 10 | env: The mix environment the app should be build for 11 | erl: The binary containing all options to pass to erl 12 | upgrade?: Is this release an upgrade? 13 | verbosity: The verbosity level, one of [silent|quiet|normal|verbose] 14 | package: Path to the generated release package. 15 | 16 | """ 17 | defstruct name: "", 18 | version: "", 19 | dev: false, 20 | env: :prod, 21 | erl: "", 22 | upgrade?: false, 23 | verbosity: :quiet, 24 | relx_config: [], 25 | package: nil 26 | end 27 | -------------------------------------------------------------------------------- /lib/exrm/deps.ex: -------------------------------------------------------------------------------- 1 | defmodule ReleaseManager.Deps do 2 | @moduledoc """ 3 | This module provides functions for retrieving dependency information. 4 | """ 5 | 6 | @doc """ 7 | Discovers missing applications which could prevent a release from running properly. 8 | Returns a tree to be formatted for output. The tree is a nested kwlist. 9 | """ 10 | def get_missing_applications(options \\ []), do: get_missing_applications(Mix.Dep.loaded([]), options) 11 | def get_missing_applications(deps, options) when is_list(deps) and is_list(options) do 12 | ignore = Keyword.get(options, :ignore, []) 13 | implicits = flatten_tree(get_implicit_applications(ignore)) 14 | project_apps = get_project_apps(Mix.Project.get!) 15 | included_apps = [{Mix.Project.config[:app], project_apps} | get_included_applications(deps)] 16 | 17 | deps 18 | |> get_dependency_tree 19 | |> map_dependency_tree(fn app_name -> Enum.find(deps, fn %Mix.Dep{app: a} -> app_name == a end) end) 20 | |> filter_dependency_tree(fn %Mix.Dep{app: app, opts: opts} -> 21 | case is_dep_required?(opts) && not app in implicits do 22 | false -> false 23 | true -> 24 | # If this app is included by another application, ignore it 25 | not Enum.any?(included_apps, fn {_, children} -> 26 | app in children 27 | end) 28 | end 29 | end) 30 | |> map_dependency_tree(fn %Mix.Dep{app: app} -> app end) 31 | end 32 | 33 | @doc """ 34 | Produces a list of lines to be printed, which display each missing application, 35 | the depedency graph representing where it comes from, and a short message describing 36 | where it should be added: 37 | 38 | ## Example 39 | 40 | exrm -> conform -> neotoma => neotoma is missing from conform 41 | exrm -> relx -> providers -> getopt => getopt is missing from providers 42 | exrm -> relx -> getopt => getopt is missing from relx 43 | exrm -> relx -> erlware_commons => erlware_commons is missing from relx 44 | exrm -> relx -> bbmustache => bbmustache is missing from relx 45 | """ 46 | def print_missing_applications(options \\ []) do 47 | deps = Mix.Dep.loaded([]) 48 | ignore = Keyword.get(options, :ignore, []) 49 | case get_missing_app_paths(get_missing_applications(deps, ignore: ignore), []) do 50 | [] -> "" 51 | missing_apps -> 52 | parents = missing_apps 53 | |> Enum.map(fn 54 | path when is_list(path) -> 55 | List.first(Enum.drop(path, Enum.count(path) - 2)) 56 | path when is_atom(path) -> 57 | path 58 | end) 59 | |> Enum.uniq 60 | |> Enum.map(fn app -> {app, Enum.find(deps, fn %Mix.Dep{app: a} -> a == app end)} end) 61 | missing_apps 62 | |> Enum.uniq 63 | |> Enum.map(fn 64 | path when is_list(path) -> 65 | parent = List.first(Enum.drop(path, Enum.count(path) - 2)) 66 | dep = Keyword.get(parents, parent) 67 | {path, dep} 68 | path when is_atom(path) -> 69 | {[path], nil} 70 | end) 71 | |> format_requirements 72 | end 73 | end 74 | 75 | # Formats the requirements built in `print_missing_applications/0` 76 | defp format_requirements(apps) do 77 | apps = Enum.map(apps, fn {app_path, from_dep} -> 78 | path_str = Enum.join(app_path, " -> ") 79 | required_app = List.last(app_path) 80 | case from_dep do 81 | nil -> 82 | {path_str, "=> #{required_app} is missing from #{Mix.Project.config[:app]}", String.length(path_str)} 83 | %Mix.Dep{} -> 84 | {path_str, "=> #{required_app} is missing from #{from_dep.app}", String.length(path_str)} 85 | end 86 | end) 87 | {_,_,pad_to} = Enum.max_by(apps, fn {_,_,len} -> len end) 88 | format_requirements(apps, Inspect.Algebra.empty, pad_to) 89 | end 90 | defp format_requirements([], doc, _pad_to) do 91 | doc 92 | |> Inspect.Algebra.nest(4 * 2) 93 | |> Inspect.Algebra.format(999) 94 | end 95 | defp format_requirements([{app, from, len}|apps], doc, pad_to) do 96 | glued = Inspect.Algebra.glue(pad_requirement(app, len, pad_to), from) 97 | doc = Inspect.Algebra.line(doc, glued) 98 | format_requirements(apps, doc, pad_to) 99 | end 100 | defp pad_requirement(app, len, pad_to) do 101 | app <> String.duplicate(" ", pad_to - len) 102 | end 103 | 104 | # Flattens the dependency graph to show paths to individual missing applications 105 | defp get_missing_app_paths([], _acc), do: [] 106 | defp get_missing_app_paths([{parent, []} | rest], acc) do 107 | [parent] ++ get_missing_app_paths(rest, acc) 108 | end 109 | defp get_missing_app_paths([{parent, children} | rest], acc) do 110 | result = get_missing_app_paths(children, [parent | acc]) 111 | result ++ get_missing_app_paths(rest, acc) 112 | end 113 | defp get_missing_app_paths([app | rest], acc) when is_atom(app) do 114 | path = Enum.reverse([app | acc]) 115 | [path | get_missing_app_paths(rest, acc)] 116 | end 117 | 118 | @doc """ 119 | Returns a list of explict applications found in mix.exs :applications/:included_applications 120 | """ 121 | def get_explicit_applications() do 122 | get_project_apps(Mix.Project.get!) 123 | end 124 | 125 | @doc """ 126 | Returns a graph (represented as a nested keyword list) of implicitly included applications 127 | for the current project. 128 | """ 129 | def get_implicit_applications(extras \\ []) do 130 | all_apps = get_included_applications() 131 | explicit = get_explicit_applications() ++ extras 132 | get_implicit_apps(explicit, all_apps, []) 133 | end 134 | 135 | @doc """ 136 | Gets all applications and what applications they include. 137 | """ 138 | def get_included_applications(), do: get_included_applications(Mix.Dep.loaded([])) 139 | def get_included_applications(deps) when is_list(deps) do 140 | deps 141 | |> Enum.map(&get_applications/1) 142 | |> List.flatten 143 | |> Enum.uniq 144 | end 145 | 146 | # Given a list of application names, and a list of all top_level applications, 147 | # this function builds a list of all applications which are implicitly included 148 | # in the release. 149 | defp get_implicit_apps([], _all_apps, acc), do: acc 150 | defp get_implicit_apps([app | rest], all_apps, acc) do 151 | case Keyword.get(all_apps, app, []) do 152 | [] -> get_implicit_apps(rest, all_apps, acc) 153 | apps -> 154 | case get_implicit_apps(apps, all_apps, []) do 155 | [] -> get_implicit_apps(rest, all_apps, [app | acc]) 156 | subapps -> get_implicit_apps(rest, all_apps, [{app, subapps} | acc]) 157 | end 158 | end 159 | end 160 | 161 | defp flatten_tree([]), do: [] 162 | defp flatten_tree(tree), do: flatten_tree(tree, []) 163 | defp flatten_tree([], acc), do: acc 164 | defp flatten_tree([element | rest], acc) when is_list(element), do: flatten_tree(rest, element++acc) 165 | defp flatten_tree([{parent, children} | rest], acc) do 166 | flatten_tree(rest, flatten_tree(children, [parent] ++ acc)) 167 | end 168 | defp flatten_tree([element | rest], acc), do: flatten_tree(rest, [element | acc]) 169 | 170 | # Gets applications for a given mix dependency 171 | defp get_applications(%Mix.Dep{app: app, manager: :mix, opts: opts}) do 172 | case is_dep_required?(opts) do 173 | false -> [] 174 | true -> 175 | project_dir = Keyword.get(opts, :dest) 176 | Mix.Project.in_project(app, project_dir, [], fn _ -> 177 | project_apps = get_project_apps(Mix.Project.get!) 178 | [{app, project_apps}] 179 | end) 180 | end 181 | end 182 | # Gets applications for a given rebar dependency 183 | defp get_applications(%Mix.Dep{app: app, manager: manager, opts: opts}) when manager in [:rebar, :make] do 184 | project_dir = Keyword.get(opts, :dest) 185 | case Path.wildcard(Path.join(project_dir, "**/#{app}.app.src")) do 186 | [] -> [] 187 | [app_src_path|_] -> 188 | case ReleaseManager.Utils.read_terms(app_src_path) do 189 | [{:application, ^app, config}] -> 190 | apps = Keyword.get(config, :applications, []) 191 | inc_apps = Keyword.get(config, :included_applications, []) 192 | [{app, apps ++ inc_apps}] 193 | _ -> 194 | [] 195 | end 196 | end 197 | end 198 | defp get_applications(%Mix.Dep{}), do: [] 199 | 200 | defp filter_dependency_tree(tree, fun), do: filter_dependency_tree(tree, fun, []) 201 | defp filter_dependency_tree([], _fun, acc), do: acc 202 | defp filter_dependency_tree([dep | rest], fun, acc) when not is_tuple(dep) do 203 | case fun.(dep) do 204 | true -> filter_dependency_tree(rest, fun, [dep | acc]) 205 | false -> filter_dependency_tree(rest, fun, acc) 206 | end 207 | end 208 | defp filter_dependency_tree([{dep, children} | rest], fun, acc) do 209 | case fun.(dep) do 210 | true -> 211 | acc = [{dep, filter_dependency_tree(children, fun, [])} | acc] 212 | filter_dependency_tree(rest, fun, acc) 213 | false -> 214 | filter_dependency_tree(rest, fun, acc) 215 | end 216 | end 217 | 218 | defp map_dependency_tree(tree, fun), do: map_dependency_tree(tree, fun, []) 219 | defp map_dependency_tree([], _fun, acc), do: acc 220 | defp map_dependency_tree([{dep, children} | rest], fun, acc) do 221 | acc = [{fun.(dep), map_dependency_tree(children, fun, [])} | acc] 222 | map_dependency_tree(rest, fun, acc) 223 | end 224 | defp map_dependency_tree([dep | rest], fun, acc) do 225 | map_dependency_tree(rest, fun, [fun.(dep) | acc]) 226 | end 227 | 228 | 229 | # Loads the current project's dependency tree and returns it 230 | # as list of lists, where elements are either atoms (no children), 231 | # or key/value pairs (parent/children). 232 | defp get_dependency_tree(deps) do 233 | deps 234 | |> Enum.map(fn dep -> get_dependency_tree(dep, deps) end) 235 | |> List.flatten 236 | end 237 | defp get_dependency_tree(%Mix.Dep{app: a, deps: deps, top_level: true} = dep, all_deps) do 238 | if {:warn_missing, false} in dep.opts do 239 | [] 240 | else 241 | [{a, get_dependency_tree(deps, all_deps, [])}] 242 | end 243 | end 244 | defp get_dependency_tree(%Mix.Dep{top_level: false}, _all_deps), do: [] 245 | defp get_dependency_tree([], _all_deps, acc), do: acc 246 | defp get_dependency_tree([%Mix.Dep{app: a} | rest], all_deps, acc) do 247 | dep = Enum.find(all_deps, fn %Mix.Dep{app: app} -> app == a end) 248 | if {:warn_missing, false} in dep.opts do 249 | [] 250 | else 251 | children = get_dependency_tree(dep.deps, all_deps, []) 252 | case children do 253 | [] -> get_dependency_tree(rest, all_deps, [dep.app | acc]) 254 | _ -> get_dependency_tree(rest, all_deps, [{dep.app, children} | acc]) 255 | end 256 | end 257 | end 258 | 259 | # Given the kwlist options from %Mix.Deps{opts: opts}, 260 | # determine if this dependency is required for the current 261 | # environment 262 | defp is_dep_required?(opts) when is_list(opts) do 263 | case Keyword.get(opts, :only) do 264 | envs when is_list(envs) -> Mix.env in envs 265 | nil -> true 266 | env -> Mix.env == env 267 | end 268 | end 269 | 270 | defp get_project_apps(mixfile) when is_atom(mixfile) do 271 | exports = mixfile.module_info(:exports) 272 | cond do 273 | {:application, 0} in exports -> 274 | app_spec = mixfile.application 275 | Keyword.get(app_spec, :applications, []) ++ Keyword.get(app_spec, :included_applications, []) 276 | :else -> 277 | [] 278 | end 279 | end 280 | 281 | end 282 | -------------------------------------------------------------------------------- /lib/exrm/plugin.ex: -------------------------------------------------------------------------------- 1 | defmodule ReleaseManager.Plugin do 2 | @moduledoc """ 3 | This module provide a simple way to add additional steps to 4 | the release task. 5 | 6 | You can define your own plugins using the sample definition below. Note that 7 | the module namespace must be nested under `ReleaseManager.Plugin.*`. 8 | 9 | defmodule ReleaseManager.Plugin.Hello do 10 | use ReleaseManager.Plugin 11 | 12 | def before_release(%Config{} = config) do 13 | info "This is executed just prior to compiling the release" 14 | end 15 | 16 | def after_release(%Config{} = config) do 17 | info "This is executed just after compiling the release" 18 | end 19 | 20 | def after_package(%Config{} = config) do 21 | info "This is executed just after packaging the release" 22 | end 23 | 24 | def after_cleanup(_args) do 25 | info "This is executed just after running cleanup" 26 | end 27 | end 28 | 29 | A couple things are imported or aliased for you. Those things are: 30 | 31 | - The ReleaseManager.Config struct is aliased for you to just Config 32 | - `debug/1`, `info/1`, `warn/1`, `notice/1`, and `error/1` are imported for you. 33 | These should be used to do any output for the user. 34 | 35 | `before_release/1` and `after_release/1` are required callbacks, and will each be passed a 36 | `Config` struct, containing the configuration for the release task. You can choose 37 | to return the config struct modified or unmodified, or not at all. In the former case, 38 | any modifications you made will be passed on to the remaining plugins and the final 39 | release task. The required callback `after_cleanup/1` is passed the command line arguments. 40 | The return value is not used. 41 | 42 | All plugins are executed just prior, and just after compiling the release, as the name of 43 | the callbacks reflect. The `before_release/1` callback is called after some internal tasks, 44 | such as generating the sys.config and others. 45 | """ 46 | use Behaviour 47 | 48 | @doc """ 49 | A plugin needs to implement `before_release/1`, and `after_release/1` 50 | both of which receive a %ReleaseManager.Config struct, as well as `after_cleanup/1`, which 51 | receives the arguments given for the command as a list of strings. 52 | """ 53 | @callback before_release(ReleaseManager.Config.t) :: any 54 | @callback after_release(ReleaseManager.Config.t) :: any 55 | @callback after_package(ReleaseManager.Config.t) :: any 56 | @callback after_cleanup([String.t]) :: any 57 | 58 | @doc false 59 | defmacro __using__(_opts) do 60 | quote do 61 | @behaviour ReleaseManager.Plugin 62 | alias ReleaseManager.Config 63 | alias ReleaseManager.Utils.Logger 64 | import Logger, only: [debug: 1, info: 1, warn: 1, notice: 1, error: 1] 65 | 66 | Module.register_attribute __MODULE__, :name, accumulate: false, persist: true 67 | Module.register_attribute __MODULE__, :moduledoc, accumulate: false, persist: true 68 | Module.register_attribute __MODULE__, :shortdoc, accumulate: false, persist: true 69 | end 70 | end 71 | 72 | @doc """ 73 | Loads all plugins in all code paths. 74 | """ 75 | @spec load_all() :: [] | [atom] 76 | def load_all, do: get_plugins(ReleaseManager.Plugin) 77 | 78 | # Loads all modules that extend a given module in the current code path. 79 | # 80 | # The convention is that it will fetch modules with the same root namespace, 81 | # and that are suffixed with the name of the module they are extending. 82 | @spec get_plugins(atom) :: [] | [atom] 83 | defp get_plugins(plugin_type) when is_atom(plugin_type) do 84 | available_modules(plugin_type) |> Enum.reduce([], &load_plugin/2) 85 | end 86 | 87 | defp load_plugin(module, modules) do 88 | if Code.ensure_loaded?(module), do: [module | modules], else: modules 89 | end 90 | 91 | defp available_modules(plugin_type) do 92 | # Ensure the current projects code path is loaded 93 | Mix.Task.run("loadpaths", []) 94 | # Fetch all .beam files 95 | Path.wildcard(Path.join([Mix.Project.build_path, "lib/**/ebin/**/*.beam"])) 96 | |> Stream.map(&String.to_char_list/1) 97 | # Parse the BEAM for behaviour implementations 98 | |> Stream.map(fn path -> 99 | case :beam_lib.chunks(path, [:attributes]) do 100 | {:ok, {mod, chunks}} -> 101 | {mod, get_in(chunks, [:attributes, :behaviour])} 102 | _ -> 103 | :error 104 | end 105 | end) 106 | # Filter out behaviours we don't care about and duplicates 107 | |> Stream.filter(fn :error -> false; {_mod, behaviours} -> is_list(behaviours) && plugin_type in behaviours end) 108 | |> Enum.uniq 109 | |> Enum.map(fn {module, _} -> module end) 110 | end 111 | end 112 | -------------------------------------------------------------------------------- /lib/exrm/plugins/appups.ex: -------------------------------------------------------------------------------- 1 | defmodule ReleaseManager.Plugin.Appups do 2 | @name "appup" 3 | @shortdoc "Generates a .appup for each dependency in your project" 4 | @moduledoc """ 5 | Generates a .appup for each dependency in your project 6 | """ 7 | 8 | use ReleaseManager.Plugin 9 | alias ReleaseManager.Config 10 | alias ReleaseManager.Utils 11 | alias ReleaseManager.Appups 12 | 13 | def before_release(%Config{upgrade?: true, env: env} = config) do 14 | Logger.notice "This is an upgrade, verifying appups exist for updated dependencies.." 15 | deps = Mix.Dep.loaded(env: env) 16 | 17 | do_appup(config, deps) 18 | config 19 | end 20 | def before_release(_), do: nil 21 | 22 | def do_appup(_config, []), do: Logger.info "All dependencies have appups ready for release!" 23 | def do_appup(config, [%Mix.Dep{app: :exrm}|deps]), do: do_appup(config, deps) 24 | def do_appup(%Config{name: project} = config, [%Mix.Dep{app: app, opts: opts}|deps]) do 25 | last_release = Utils.get_last_release(project) 26 | last_release_definition = Utils.rel_dest_path [project, "releases", last_release, "#{project}.rel"] 27 | [{:release, _app, _erts, apps}] = Utils.read_terms(last_release_definition) 28 | case List.keyfind(apps, app, 0) do 29 | nil -> :ok 30 | app_info -> 31 | last_app_version = "#{elem(app_info, 1)}" 32 | v1_path = Utils.rel_dest_path [project, "lib", "#{app}-#{last_app_version}"] 33 | v2_path = Keyword.get(opts, :build) 34 | v2_ebin_path = Path.join(v2_path, "ebin") 35 | 36 | [{:application, _app, info}] = Path.join(v2_ebin_path, "#{app}.app") |> Utils.read_terms 37 | current_app_version = "#{Keyword.get(info, :vsn)}" 38 | appup_path = Path.join(v2_ebin_path, "#{app}.appup") 39 | appup_exists? = File.exists?(appup_path) 40 | 41 | cond do 42 | current_app_version == last_app_version -> :ok 43 | appup_exists? -> 44 | Logger.debug "#{app} requires an appup, and one was provided, skipping generation.." 45 | true -> 46 | Logger.debug "#{app} requires an appup, but it wasn't provided, one will be generated for you.." 47 | case Appups.make(app, last_app_version, current_app_version, v1_path, v2_path) do 48 | {:error, reason} -> 49 | Logger.error "Failed to generate appup for #{app}: #{reason}" 50 | {:ok, _appup} -> 51 | Logger.info "Generated .appup for #{app} #{last_app_version} -> #{current_app_version}" 52 | end 53 | end 54 | end 55 | 56 | do_appup(config, deps) 57 | end 58 | 59 | def after_release(_), do: nil 60 | def after_package(_), do: nil 61 | def after_cleanup(_), do: nil 62 | 63 | end 64 | -------------------------------------------------------------------------------- /lib/exrm/plugins/consolidation.ex: -------------------------------------------------------------------------------- 1 | defmodule ReleaseManager.Plugin.Consolidation do 2 | @name "protocol.consolidation" 3 | @shortdoc "Performs protocol consolidation for your release." 4 | 5 | use ReleaseManager.Plugin 6 | alias ReleaseManager.Config 7 | alias ReleaseManager.Utils 8 | import ReleaseManager.Utils, except: [debug: 1, info: 1, warn: 1, error: 1] 9 | 10 | def before_release(%Config{verbosity: verbosity, env: env} = config) do 11 | build_embedded = Keyword.get(Mix.Project.config, :build_embedded, false) 12 | should_compile = env != :test && !build_embedded 13 | if should_compile do 14 | debug "Performing protocol consolidation..." 15 | with_env env, fn -> 16 | cond do 17 | verbosity == :verbose -> 18 | mix "compile.protocols", env, :verbose 19 | true -> 20 | mix "compile.protocols", env 21 | end 22 | end 23 | end 24 | 25 | # Load relx.config 26 | if env != :test do 27 | debug "Packaging consolidated protocols..." 28 | 29 | # Add overlay to relx.config which copies consolidated dir to release 30 | consolidated_path = Path.join([Mix.Project.build_path, "consolidated"]) 31 | case File.ls(consolidated_path) do 32 | {:error, _} -> 33 | config 34 | {:ok, filenames} -> 35 | dest_path = "lib/#{config.name}-#{config.version}/consolidated" 36 | overlays = [overlay: Enum.map(filenames, fn name -> 37 | {:copy, '#{consolidated_path}/#{name}', '#{Path.join([dest_path, name])}'} 38 | end)] 39 | updated = Utils.merge(config.relx_config, overlays) 40 | %{config | :relx_config => updated} 41 | end 42 | else 43 | config 44 | end 45 | end 46 | 47 | def after_release(_), do: nil 48 | def after_package(_), do: nil 49 | def after_cleanup(_), do: nil 50 | end 51 | -------------------------------------------------------------------------------- /lib/exrm/utils/logger.ex: -------------------------------------------------------------------------------- 1 | defmodule ReleaseManager.Utils.Logger do 2 | 3 | def configure(verbosity) when is_atom(verbosity) do 4 | Application.put_env(:exrm, :verbosity, verbosity) 5 | end 6 | 7 | @doc "Print an informational message without color" 8 | def debug(message), do: log(:debug, IO.ANSI.format(["==> ", message])) 9 | @doc "Print an informational message in green" 10 | def info(message), do: log(:info, IO.ANSI.format(["==> ", :green, message])) 11 | @doc "Print a warning message in yellow" 12 | def warn(message), do: log(:warn, IO.ANSI.format(["==> ", :yellow, message])) 13 | @doc "Print a notice in yellow" 14 | def notice(message), do: log(:notice, IO.ANSI.format([:yellow, message])) 15 | @doc "Print an error message in red" 16 | def error(message), do: log(:error, IO.ANSI.format(["==> ", :red, message])) 17 | 18 | defp log(level, message), do: log(level, Application.get_env(:exrm, :verbosity, :normal), message) 19 | 20 | defp log(:error, :silent, message), do: IO.puts message 21 | defp log(_level, :silent, _message), do: :ok 22 | defp log(:debug, :quiet, _message), do: :ok 23 | defp log(:debug, :normal, _message), do: :ok 24 | defp log(:debug, _verbosity, message), do: IO.puts message 25 | defp log(:info, :quiet, _message), do: :ok 26 | defp log(:info, _verbosity, message), do: IO.puts message 27 | defp log(_level, _verbosity, message), do: IO.puts message 28 | 29 | end 30 | -------------------------------------------------------------------------------- /lib/exrm/utils/utils.ex: -------------------------------------------------------------------------------- 1 | defmodule ReleaseManager.Utils do 2 | @moduledoc """ 3 | This module provides helper functions for the `mix release` and 4 | `mix release.clean` tasks. 5 | """ 6 | import Mix.Shell, only: [cmd: 2] 7 | alias ReleaseManager.Utils.Logger 8 | 9 | # Relx constants 10 | @relx_output_path "rel" 11 | 12 | @doc """ 13 | Perform some actions within the context of a specific mix environment 14 | """ 15 | def with_env(env, fun) do 16 | old_env = Mix.env 17 | try do 18 | # Change env 19 | Mix.env(env) 20 | fun.() 21 | after 22 | # Change back 23 | Mix.env(old_env) 24 | end 25 | end 26 | 27 | @doc """ 28 | Load the current project's configuration 29 | """ 30 | def load_config(env, project_config \\ Mix.Project.config) do 31 | config_path = Keyword.get(project_config, :config_path, "config/config.exs") 32 | with_env env, fn -> 33 | if File.regular?(config_path) do 34 | Mix.Config.read! config_path 35 | else 36 | [] 37 | end 38 | end 39 | end 40 | 41 | @doc """ 42 | Call the _elixir mix binary with the given arguments 43 | """ 44 | def mix(command, :quiet), do: mix(command, :dev, :quiet) 45 | def mix(command, :verbose), do: mix(command, :dev, :verbose) 46 | def mix(command, env), do: mix(command, env, :quiet) 47 | def mix(command, env, :quiet) do 48 | case :os.type() do 49 | {:nt} -> do_cmd("(set MIX_ENV=#{env}) & (mix #{command})", &ignore/1) 50 | {:win32, :nt} -> do_cmd("(set MIX_ENV=#{env}) & (mix #{command})", &ignore/1) 51 | _ -> do_cmd("MIX_ENV=#{env} mix #{command}", &ignore/1) 52 | end 53 | end 54 | def mix(command, env, :verbose) do 55 | case :os.type() do 56 | {:nt} -> do_cmd("(set MIX_ENV=#{env}) & (mix #{command})", &IO.write/1) 57 | {:win32, :nt} -> do_cmd("(set MIX_ENV=#{env}) & (mix #{command})", &IO.write/1) 58 | _ -> do_cmd("MIX_ENV=#{env} mix #{command}", &IO.write/1) 59 | end 60 | end 61 | @doc """ 62 | Change user permissions for a target file or directory 63 | """ 64 | def chmod(target, mode) do 65 | case File.chmod(target, mode) do 66 | :ok -> :ok 67 | {:error, _} -> :ok 68 | end 69 | end 70 | @doc """ 71 | Execute `relx` 72 | """ 73 | def relx(name, version, verbosity, upgrade?, dev_mode?) do 74 | # Setup paths 75 | config = rel_file_dest_path "relx.config" 76 | output_dir = @relx_output_path |> Path.expand 77 | # Convert friendly verbosity names to relx values 78 | v = case verbosity do 79 | :silent -> 0 80 | :quiet -> 1 81 | :normal -> 2 82 | :verbose -> 3 83 | _ -> 2 # Normal if we get an odd value 84 | end 85 | # Let relx do the heavy lifting 86 | relx_args = [ 87 | log_level: v, 88 | root_dir: '#{File.cwd!}', 89 | config: '#{config}', 90 | relname: '#{name}', 91 | relvsn: '#{version}', 92 | output_dir: '#{output_dir}', 93 | dev_mode: dev_mode? 94 | ] 95 | result = cond do 96 | upgrade? && dev_mode? -> 97 | last_release = get_last_release(name) 98 | :relx.do [{:upfrom, '#{last_release}'} | relx_args], ['release', 'relup'] 99 | upgrade? -> 100 | last_release = get_last_release(name) 101 | :relx.do [{:upfrom, '#{last_release}'} | relx_args], ['release', 'relup', 'tar'] 102 | dev_mode? -> 103 | :relx.do relx_args, ['release'] 104 | true -> 105 | :relx.do relx_args, ['release', 'tar'] 106 | end 107 | case result do 108 | {:ok, _state} -> :ok 109 | {:error, e} -> 110 | case e do 111 | {:rlx_prv_release, :no_goals_specified} -> 112 | {:error, "No goals have been specified for this release!"} 113 | {:rlx_prv_release, {:release_erts_error, dir}} -> 114 | {:error, "ERTS could not be found in #{dir}"} 115 | {:rlx_prv_release, {:no_release_name, vsn}} -> 116 | {:error, "A target release version was specified (#{vsn}) but no name."} 117 | {:rlx_prv_release, {:invalid_release_info, info}} -> 118 | {:error, "Target release information is in an invalid format:\n#{inspect info}"} 119 | {:rlx_prv_release, {:multiple_release_names, a, b}} -> 120 | {:error, "Multiple releases are defined, but no default was specified: #{a}, #{b}"} 121 | {:rlx_prv_release, :no_releases_in_system} -> 122 | {:error, "No releases have been defined! See the debug output for more information."} 123 | {:rlx_prv_release, {:no_releases_for, name}} -> 124 | {:error, "No releases exist for #{name}. See the debug output for more information."} 125 | {:rlx_prv_release, {:release_not_found, {name, vsn}}} -> 126 | {:error, "No such release: #{name}-#{vsn}. See the debug output for more information."} 127 | {:rlx_prv_release, {:failed_solve, {:unreachable_package, missing_app}}} -> 128 | {:error, "Unable to find application #{missing_app}. See the debug output for more information."} 129 | {:rlx_prv_relup, {:relup_generation_error, current_name, upfrom_name}}-> 130 | {:error, "Unknown internal release error generating the relup from #{upfrom_name} to #{current_name}. See debug output."} 131 | {:rlx_prv_relup, {:relup_generation_warning, module, warnings}}-> 132 | {:error, "Warnings generating relup:\n#{:rlx_util.indent(2)}#{module.format_warning(warnings)}"} 133 | {:rlx_prv_relup, {:no_upfrom_release_found, :undefined}} -> 134 | {:error, "Could not find any previous versions of the release to upgrade!"} 135 | {:rlx_prv_relup, {:no_upfrom_release_found, vsn}} -> 136 | {:error, "Could not find find release version #{vsn} for relup!"} 137 | {:rlx_prv_relup, {:relup_script_generation_error, {:relup_script_generator_error, :systools_relup, {:missing_sasl, _}}}} -> 138 | {:error, "Unfortunately, due to requirements in systools, you need to have the sasl application \n in both current release and the release to upgrade from."} 139 | {:rlx_prv_relup, {:relup_script_generation_error, module, errors}}-> 140 | {:error, "Failed to generate relup: #{:rlx_util.indent(2)}#{module.format_error(errors)}"} 141 | {:rlx_prv_archive, {:tar_unknown_generation_error, module, vsn}}-> 142 | {:error, "Unknown error occurred when generating tarball for #{module}-#{vsn}\n Do you have any file names longer than 100 characters? That is a known issue with systools."} 143 | {:rlx_prv_archive, {:tar_generation_warn, module, warnings}}-> 144 | {:error, "Warnings were reported when generating the release tarball:\n#{:rlx_util.indent(2)}#{module}: #{inspect warnings}"} 145 | {:rlx_prv_archive, {:tar_generation_error, module, errors}}-> 146 | {:error, "Errors occurred when generating the release tarball:\n#{:rlx_util.indent(2)}#{module}: #{inspect errors}"} 147 | {:rlx_app_info, {:vsn_parse, app}}-> 148 | {:error, "Could not parse version for #{app}"} 149 | {_relx_module, _unhandled_err} -> 150 | {:error, "Failed to build release. See the debug output for specifics."} 151 | end 152 | end 153 | end 154 | 155 | @doc "Exits with exit status 1" 156 | def abort!, do: exit({:shutdown, 1}) 157 | 158 | @doc """ 159 | Get a list of tuples representing the previous releases: 160 | 161 | ## Examples 162 | 163 | get_releases #=> [{"test", "0.0.1"}, {"test", "0.0.2"}] 164 | 165 | """ 166 | def get_releases(project) do 167 | release_path = Path.join([File.cwd!, "rel", project, "releases"]) 168 | case release_path |> File.exists? do 169 | false -> [] 170 | true -> 171 | release_path 172 | |> File.ls! 173 | |> Enum.reject(fn entry -> entry in ["RELEASES", "start_erl.data"] end) 174 | |> Enum.map(fn version -> {project, version} end) 175 | end 176 | end 177 | 178 | @doc """ 179 | Get the most recent release prior to the current one 180 | """ 181 | def get_last_release(project) do 182 | hd(project |> get_releases |> Enum.map(fn {_, v} -> v end) |> sort_versions) 183 | end 184 | 185 | @doc """ 186 | Sort a list of versions, latest one first. Tries to use semver version 187 | compare, but can fall back to regular string compare. 188 | """ 189 | def sort_versions(versions) do 190 | versions 191 | |> Enum.map(fn ver -> 192 | # Special handling for git-describe versions 193 | compared = case Regex.named_captures(~r/(?\d+\.\d+\.\d+)-(?\d+)-(?[A-Ga-g0-9]+)/, ver) do 194 | nil -> 195 | {:standard, ver, nil} 196 | %{"ver" => version, "commits" => n, "sha" => sha} -> 197 | {:describe, <>, String.to_integer(n)} 198 | end 199 | {ver, compared} 200 | end) 201 | |> Enum.sort( 202 | fn {_, {v1type, v1str, v1_commits_since}}, {_, {v2type, v2str, v2_commits_since}} -> 203 | case { parse_version(v1str), parse_version(v2str) } do 204 | {{:semantic, v1}, {:semantic, v2}} -> 205 | case Version.compare(v1, v2) do 206 | :gt -> true 207 | :eq -> 208 | case {v1type, v2type} do 209 | {:standard, :standard} -> v1 > v2 # probably always false 210 | {:standard, :describe} -> false # v2 is an incremental version over v1 211 | {:describe, :standard} -> true # v1 is an incremental version over v2 212 | {:describe, :describe} -> # need to parse out the bits 213 | v1_commits_since > v2_commits_since 214 | end 215 | :lt -> false 216 | end; 217 | {{_, v1}, {_, v2}} -> 218 | v1 > v2 219 | end 220 | end) 221 | |> Enum.map(fn {v, _} -> v end) 222 | end 223 | 224 | defp parse_version(ver) do 225 | case Version.parse(ver) do 226 | {:ok, semver} -> {:semantic, semver} 227 | :error -> {:nonsemantic, ver} 228 | end 229 | end 230 | 231 | @doc """ 232 | Get the local paths of the current Elixir libraries 233 | """ 234 | def get_elixir_lib_paths() do 235 | [elixir_lib_path, _] = String.split("#{:code.which(:elixir)}", "/elixir/ebin/elixir.beam") 236 | elixir_lib_path 237 | |> Path.expand 238 | |> File.ls! 239 | |> Enum.map(&(Path.join(elixir_lib_path, &1 <> "/ebin"))) 240 | end 241 | 242 | @doc """ 243 | Writes an Elixir/Erlang term to the provided path 244 | """ 245 | def write_term(path, term) do 246 | :file.write_file('#{path}', :io_lib.fwrite('~p.\n', [term])) 247 | end 248 | 249 | @doc """ 250 | Writes a collection of Elixir/Erlang terms to the provided path 251 | """ 252 | def write_terms(path, terms) when is_list(terms) do 253 | format_str = String.duplicate("~p.\n\n", Enum.count(terms)) |> String.to_char_list 254 | :file.write_file('#{path}', :io_lib.fwrite(format_str, terms |> Enum.reverse), [encoding: :utf8]) 255 | end 256 | 257 | @doc """ 258 | Reads a file as Erlang terms 259 | """ 260 | def read_terms(path) do 261 | result = case '#{path}' |> :file.consult do 262 | {:ok, terms} -> 263 | terms 264 | {:error, {line, type, msg}} -> 265 | Logger.error "Unable to parse #{path}: Line #{line}, #{type}, - #{msg}" 266 | abort!() 267 | {:error, reason} -> 268 | Logger.error "Unable to access #{path}: #{reason}" 269 | abort!() 270 | end 271 | result 272 | end 273 | 274 | @doc """ 275 | Convert a string to Erlang terms 276 | """ 277 | def string_to_terms(str) do 278 | str 279 | |> String.split("}.") 280 | |> Stream.map(&(String.strip(&1, ?\n))) 281 | |> Stream.map(&String.strip/1) 282 | |> Stream.map(&('#{&1}}.')) 283 | |> Stream.map(&(:erl_scan.string(&1))) 284 | |> Stream.map(fn {:ok, tokens, _} -> :erl_parse.parse_term(tokens) end) 285 | |> Stream.filter(fn {:ok, _} -> true; {:error, _} -> false end) 286 | |> Enum.reduce([], fn {:ok, term}, acc -> [term|acc] end) 287 | |> Enum.reverse 288 | end 289 | 290 | @doc """ 291 | Merges two sets of Elixir/Erlang terms, where the terms come 292 | in the form of lists of tuples. For example, such as is found 293 | in the relx.config file 294 | """ 295 | def merge(old, new) when is_list(old) and is_list(new) do 296 | merge(old, new, []) 297 | end 298 | 299 | defp merge([h|t], new, acc) when is_tuple(h) do 300 | case :lists.keytake(elem(h, 0), 1, new) do 301 | {:value, new_value, rest} -> 302 | # Value is present in new, so merge the value 303 | merged = merge_term(h, new_value) 304 | merge(t, rest, [merged|acc]) 305 | false -> 306 | # Value doesn't exist in new, so add it 307 | merge(t, new, [h|acc]) 308 | end 309 | end 310 | defp merge([], new, acc) do 311 | Enum.reverse(acc, new) 312 | end 313 | 314 | defp merge_term(old, new) when is_tuple(old) and is_tuple(new) do 315 | old 316 | |> Tuple.to_list 317 | |> Enum.with_index 318 | |> Enum.reduce([], fn 319 | {[], idx}, acc -> 320 | [elem(new, idx)|acc] 321 | {val, idx}, acc when is_list(val) -> 322 | case :io_lib.char_list(val) do 323 | true -> 324 | [elem(new, idx)|acc] 325 | false -> 326 | merged = val |> Enum.concat(elem(new, idx)) |> Enum.uniq 327 | [merged|acc] 328 | end 329 | {val, idx}, acc when is_tuple(val) -> 330 | [merge_term(val, elem(new, idx))|acc] 331 | {_val, idx}, acc -> 332 | [elem(new, idx)|acc] 333 | end) 334 | |> Enum.reverse 335 | |> List.to_tuple 336 | end 337 | 338 | @doc "Get the priv path of the exrm dependency" 339 | def priv_path, do: "#{:code.priv_dir('exrm')}" 340 | @doc "Get the priv/rel path of the exrm dependency" 341 | def rel_source_path, do: Path.join(priv_path(), "rel") 342 | @doc "Get the path to a file located in priv/rel of the exrm dependency" 343 | def rel_source_path(file), do: Path.join(rel_source_path(), file) 344 | @doc "Get the priv/rel/files path of the exrm dependency" 345 | def rel_file_source_path, do: Path.join([priv_path(), "rel", "files"]) 346 | @doc "Get the path to a file located in priv/rel/files of the exrm dependency" 347 | def rel_file_source_path(file), do: Path.join(rel_file_source_path(), file) 348 | @doc """ 349 | Get the path to a file located in the rel directory of the current project. 350 | You can pass either a file name, or a list of directories to a file, like: 351 | 352 | iex> ReleaseManager.Utils.rel_dest_path "relx.config" 353 | "path/to/project/rel/relx.config" 354 | 355 | iex> ReleaseManager.Utils.rel_dest_path ["", "lib", ".appup"] 356 | "path/to/project/rel//lib/.appup" 357 | 358 | """ 359 | def rel_dest_path(files) when is_list(files), do: Path.join([rel_dest_path()] ++ files) 360 | def rel_dest_path(file), do: Path.join(rel_dest_path(), file) 361 | @doc "Get the rel path of the current project." 362 | def rel_dest_path, do: Path.join(File.cwd!, "rel") 363 | @doc """ 364 | Get the path to a file located in the rel/.files directory of the current project. 365 | You can pass either a file name, or a list of directories to a file, like: 366 | 367 | iex> ReleaseManager.Utils.rel_file_dest_path "sys.config" 368 | "path/to/project/rel/.files/sys.config" 369 | 370 | iex> ReleaseManager.Utils.rel_dest_path ["some", "path", "file.txt"] 371 | "path/to/project/rel/.files/some/path/file.txt" 372 | 373 | """ 374 | def rel_file_dest_path(files) when is_list(files), do: Path.join([rel_file_dest_path()] ++ files) 375 | def rel_file_dest_path(file), do: Path.join(rel_file_dest_path(), file) 376 | @doc "Get the rel/.files path of the current project." 377 | def rel_file_dest_path, do: Path.join([File.cwd!, "rel", ".files"]) 378 | 379 | # Ignore a message when used as the callback for Mix.Shell.cmd 380 | defp ignore(_), do: nil 381 | 382 | defp do_cmd(command, callback) do 383 | case cmd(command, callback) do 384 | 0 -> :ok 385 | _ -> {:error, "Release step failed. Please fix any errors and try again."} 386 | end 387 | end 388 | 389 | end 390 | -------------------------------------------------------------------------------- /lib/mix/tasks/release.clean.ex: -------------------------------------------------------------------------------- 1 | defmodule Mix.Tasks.Release.Clean do 2 | @moduledoc """ 3 | Clean up any release-related files. 4 | 5 | ## Examples 6 | 7 | # Cleans the release for the current version of the project 8 | mix release.clean 9 | 10 | # Remove all files generated by exrm, including releases 11 | mix release.clean --implode 12 | 13 | # Implode, but do not confirm (DANGEROUS) 14 | mix release.clean --implode --no-confirm 15 | 16 | """ 17 | @shortdoc "Clean up any release-related files" 18 | 19 | use Mix.Task 20 | alias ReleaseManager.Utils.Logger 21 | import ReleaseManager.Utils 22 | 23 | def run(args) do 24 | if Mix.Project.umbrella? do 25 | config = [build_path: Mix.Project.build_path] 26 | for %Mix.Dep{app: app, opts: opts} <- Mix.Dep.Umbrella.loaded do 27 | Mix.Project.in_project(app, opts[:path], config, fn _ -> do_run(args) end) 28 | end 29 | else 30 | do_run(args) 31 | end 32 | end 33 | 34 | def do_run(args) do 35 | app = Mix.Project.config |> Keyword.get(:app) 36 | version = Mix.Project.config |> Keyword.get(:version) 37 | Logger.debug "Removing release files for #{app}-#{version}..." 38 | cond do 39 | "--implode" in args -> 40 | if "--no-confirm" in args or confirm_implode?(app) do 41 | do_cleanup :all 42 | execute_after_hooks(args) 43 | Logger.info "All release files for #{app}-#{version} were removed successfully!" 44 | end 45 | true -> 46 | do_cleanup :build 47 | execute_after_hooks(args) 48 | Logger.info "The release for #{app}-#{version} has been removed." 49 | end 50 | end 51 | 52 | # Clean release build 53 | def do_cleanup(:build) do 54 | project = Mix.Project.config |> Keyword.get(:app) |> Atom.to_string 55 | version = Mix.Project.config |> Keyword.get(:version) 56 | build = Path.absname("../prod", Mix.Project.build_path) 57 | release = rel_dest_path [project, "releases", version] 58 | releases = rel_dest_path [project, "releases", "RELEASES"] 59 | start_erl = rel_dest_path [project, "releases", "start_erl.data"] 60 | lib = rel_dest_path [project, "lib", "#{project}-#{version}"] 61 | relup = rel_dest_path [project, "relup"] 62 | 63 | if File.exists?(release), do: File.rm_rf!(release) 64 | if File.exists?(releases), do: File.rm_rf!(releases) 65 | if File.exists?(start_erl), do: File.rm_rf!(start_erl) 66 | if File.exists?(lib), do: File.rm_rf!(lib) 67 | if File.exists?(relup), do: File.rm_rf!(relup) 68 | if Mix.env != :prod && File.exists?(build) do 69 | build 70 | |> File.ls! 71 | |> Enum.map(fn dir -> build |> Path.join(dir) end) 72 | |> Enum.map(&File.rm_rf!/1) 73 | end 74 | end 75 | # Clean up the template files for release generation 76 | def do_cleanup(:relfiles) do 77 | rel_files = rel_file_dest_path() 78 | if File.exists?(rel_files), do: File.rm_rf!(rel_files) 79 | end 80 | # Clean up everything 81 | def do_cleanup(:all) do 82 | # Execute other clean tasks 83 | do_cleanup :build 84 | 85 | # Remove release folder 86 | rel = rel_dest_path() 87 | if File.exists?(rel), do: File.rm_rf!(rel) 88 | end 89 | 90 | defp execute_after_hooks(args) do 91 | plugins = ReleaseManager.Plugin.load_all 92 | Enum.each plugins, fn plugin -> 93 | try do 94 | plugin.after_cleanup(args) 95 | rescue 96 | exception -> 97 | stacktrace = System.stacktrace 98 | Logger.error "Failed to execute after_cleanup hook for #{plugin}!" 99 | reraise exception, stacktrace 100 | end 101 | end 102 | end 103 | 104 | defp confirm_implode?(app) do 105 | IO.puts IO.ANSI.yellow 106 | msg = """ 107 | THIS WILL REMOVE ALL RELEASES AND RELATED CONFIGURATION FOR #{app |> Atom.to_string |> String.upcase}! 108 | Are you absolutely sure you want to proceed? 109 | """ 110 | answer = IO.gets(msg <> " [Yn]: ") |> String.rstrip(?\n) 111 | IO.puts IO.ANSI.reset 112 | answer =~ ~r/^(Y(es)?)?$/i 113 | end 114 | 115 | end 116 | -------------------------------------------------------------------------------- /lib/mix/tasks/release.ex: -------------------------------------------------------------------------------- 1 | defmodule Mix.Tasks.Release do 2 | @moduledoc """ 3 | Build a release for the current mix application. 4 | 5 | ## Examples 6 | 7 | # Build a release using defaults 8 | mix release 9 | 10 | # Pass args to erlexec when running the release 11 | mix release --erl="-env TZ UTC" 12 | 13 | # Enable dev mode. Make changes, compile using MIX_ENV=prod 14 | # and execute your release again to pick up the changes 15 | mix release --dev 16 | 17 | # Set the verbosity level 18 | mix release --verbosity=[silent|quiet|normal|verbose] 19 | 20 | # Do not ask for confirmation to skip missing applications warning 21 | mix release --no-confirm-missing 22 | 23 | You may pass any number of arguments as needed. Make sure you pass arguments 24 | using `--key=value`, not `--key value`, as the args may be interpreted incorrectly 25 | otherwise. 26 | 27 | """ 28 | @shortdoc "Build a release for the current mix application" 29 | 30 | use Mix.Task 31 | import ExUnit.CaptureIO 32 | import ReleaseManager.Utils 33 | alias ReleaseManager.Utils 34 | alias ReleaseManager.Utils.Logger 35 | alias ReleaseManager.Config 36 | alias ReleaseManager.Deps 37 | 38 | @_RELXCONF "relx.config" 39 | @_BOOT_FILE "boot" 40 | @_NODETOOL "nodetool" 41 | @_SYSCONFIG "sys.config" 42 | @_VMARGS "vm.args" 43 | @_RELEASE_DEF "release_definition.txt" 44 | @_RELEASES "{{{RELEASES}}}" 45 | @_NAME "{{{PROJECT_NAME}}}" 46 | @_VERSION "{{{PROJECT_VERSION}}}" 47 | @_ERTS_VSN "{{{ERTS_VERSION}}}" 48 | @_ERL_OPTS "{{{ERL_OPTS}}}" 49 | @_LIB_DIRS "{{{LIB_DIRS}}}" 50 | 51 | def run(args) do 52 | Mix.Project.compile(args) 53 | 54 | if Mix.Project.umbrella? do 55 | config = [umbrella?: true] 56 | for %Mix.Dep{app: app, opts: opts} <- Mix.Dep.Umbrella.loaded do 57 | Mix.Project.in_project(app, opts[:path], config, fn _ -> do_run(args) end) 58 | end 59 | else 60 | do_run(args) 61 | end 62 | end 63 | 64 | defp do_run(args) do 65 | # Start with a clean slate 66 | Mix.Tasks.Release.Clean.do_cleanup(:build) 67 | # Collect release configuration 68 | config = parse_args(args) 69 | Logger.notice "Building release with MIX_ENV=#{config.env}." 70 | # Begin release pipeline 71 | config 72 | |> check_applications(args) 73 | |> generate_relx_config 74 | |> generate_sys_config 75 | |> generate_vm_args 76 | |> generate_boot_script 77 | |> execute_before_hooks 78 | |> do_release 79 | |> generate_nodetool 80 | |> generate_install_escript 81 | |> execute_after_hooks 82 | |> update_release_package 83 | |> execute_package_hooks 84 | 85 | Logger.info "The release for #{config.name}-#{config.version} is ready!" 86 | Logger.info "You can boot a console running your release with `$ rel/#{config.name}/bin/#{config.name} console`" 87 | end 88 | 89 | defp check_applications(%Config{} = config, args) do 90 | case Deps.print_missing_applications(ignore: [:exrm]) do 91 | "" -> config 92 | output -> 93 | IO.puts IO.ANSI.yellow 94 | IO.puts "You have dependencies (direct/transitive) which are not in :applications!" 95 | IO.puts "The following apps should be added to :applications in mix.exs:\n#{output}#{IO.ANSI.reset}\n" 96 | case "--no-confirm-missing" in args do 97 | true -> 98 | config 99 | false -> 100 | msg = IO.ANSI.yellow <> "Continue anyway? Your release may not work as expected if these dependencies are required!" 101 | answer = IO.gets(msg <> " [Yn]: ") |> String.rstrip(?\n) 102 | IO.puts IO.ANSI.reset 103 | case answer =~ ~r/^(Y(es)?)?$/i do 104 | true -> config 105 | false -> abort!() 106 | end 107 | end 108 | end 109 | end 110 | 111 | defp generate_relx_config(%Config{name: name, version: version} = config) do 112 | Logger.debug "Generating relx configuration..." 113 | # Get paths 114 | rel_def = rel_file_source_path @_RELEASE_DEF 115 | source = rel_source_path @_RELXCONF 116 | # Get relx.config template contents 117 | relx_config = source |> File.read! 118 | # Get release definition template contents 119 | tmpl = rel_def |> File.read! 120 | # Generate release configuration for historical releases 121 | releases = get_releases(name) 122 | |> Enum.map(fn {rname, rver} -> tmpl |> replace_release_info(rname, rver) end) 123 | |> Enum.join 124 | # Set upgrade flag if this is an upgrade release 125 | config = case releases do 126 | "" -> config 127 | _ -> %{config | :upgrade? => true} 128 | end 129 | elixir_paths = get_elixir_lib_paths() |> Enum.map(&String.to_char_list/1) 130 | lib_dirs = [ '#{Path.join(Mix.Project.build_path, "lib")}', '#{Mix.Project.deps_path}' | elixir_paths ] 131 | # Build release configuration 132 | relx_config = relx_config 133 | |> String.replace(@_RELEASES, releases) 134 | |> String.replace(@_LIB_DIRS, :io_lib.fwrite('~p.\n\n', [{:lib_dirs, lib_dirs}]) |> List.to_string) 135 | # Replace placeholders for current release 136 | relx_config = relx_config |> replace_release_info(name, version) 137 | # Read config as Erlang terms 138 | relx_config = Utils.string_to_terms(relx_config) 139 | # Merge user provided relx.config 140 | user_config_path = rel_dest_path @_RELXCONF 141 | merged = case user_config_path |> File.exists? do 142 | true -> 143 | Logger.debug "Merging custom relx configuration from #{user_config_path |> Path.relative_to_cwd}..." 144 | case Utils.read_terms(user_config_path) do 145 | [] -> relx_config 146 | [{_, _}|_] = user_config -> Utils.merge(relx_config, user_config) 147 | [user_config] when is_list(user_config) -> Utils.merge(relx_config, user_config) 148 | [user_config] -> Utils.merge(relx_config, [user_config]) 149 | end 150 | _ -> 151 | relx_config 152 | end 153 | # Save relx config for use later 154 | %{config | :relx_config => merged} 155 | end 156 | 157 | defp generate_sys_config(%Config{env: env} = config) do 158 | default_sysconfig = rel_file_source_path @_SYSCONFIG 159 | user_sysconfig = rel_dest_path @_SYSCONFIG 160 | dest = rel_file_dest_path @_SYSCONFIG 161 | 162 | Logger.debug "Generating sys.config..." 163 | # Read in current project config 164 | project_conf = load_config(env) 165 | # Merge project config with either the user-provided config, or the default sys.config we provide. 166 | # If a sys.config is provided by the user, it will take precedence over project config. If the 167 | # default sys.config is used, the project config will take precedence instead. 168 | merged = case user_sysconfig |> File.exists? do 169 | true -> 170 | Logger.debug "Merging custom sys.config from #{user_sysconfig |> Path.relative_to_cwd}..." 171 | # User-provided 172 | case user_sysconfig |> Utils.read_terms do 173 | [] -> project_conf 174 | [user_conf] when is_list(user_conf) -> Mix.Config.merge(project_conf, user_conf) 175 | [user_conf] -> Mix.Config.merge(project_conf, [user_conf]) 176 | end 177 | _ -> 178 | # Default 179 | [default_conf] = default_sysconfig |> Utils.read_terms 180 | Mix.Config.merge(default_conf, project_conf) 181 | end 182 | # Ensure parent directory exists prior to writing 183 | File.mkdir_p!(dest |> Path.dirname) 184 | # Write the config to disk 185 | dest |> Utils.write_term(merged) 186 | # tighten permissions on sys.config to owner read/write 187 | dest |> File.chmod(0o0600) 188 | # Continue.. 189 | config 190 | end 191 | 192 | defp generate_vm_args(%Config{version: version, relx_config: relx_config} = config) do 193 | Logger.debug "Generating vm.args..." 194 | vmargs_path = rel_dest_path(@_VMARGS) 195 | vmargs_path = case File.exists?(vmargs_path) do 196 | false -> 197 | src_path = rel_file_source_path(@_VMARGS) 198 | dest_path = rel_file_dest_path(@_VMARGS) 199 | contents = File.read!(src_path) |> String.replace(@_NAME, config.name) 200 | File.write!(dest_path, contents) 201 | dest_path 202 | true -> 203 | vmargs_path 204 | end 205 | overlays = [overlay: [ 206 | {:copy, String.to_char_list(vmargs_path), 'releases/#{version}/vm.args'} 207 | ]] 208 | # Update configuration to add new overlay for vm.args 209 | updated = Utils.merge(relx_config, overlays) 210 | %{config | :relx_config => updated} 211 | end 212 | 213 | defp generate_boot_script(%Config{name: name, version: version, erl: erl_opts} = config) do 214 | erts = extract_erts_version(config) 215 | boot = rel_file_source_path @_BOOT_FILE 216 | winboot = rel_file_source_path "#{@_BOOT_FILE}.bat" 217 | dest = rel_file_dest_path @_BOOT_FILE 218 | windest = rel_file_dest_path "#{@_BOOT_FILE}.bat" 219 | shim = rel_file_source_path "boot_shim" 220 | winshim = rel_file_source_path "boot_shim.bat" 221 | shim_dest = rel_file_dest_path "boot_shim" 222 | winshim_dest = rel_file_dest_path "boot_shim.bat" 223 | 224 | Logger.debug "Generating boot script..." 225 | 226 | [{boot, dest}, {winboot, windest}, {shim, shim_dest}, {winshim, winshim_dest}] 227 | |> Enum.each(fn {infile, outfile} -> 228 | contents = File.read!(infile) 229 | |> String.replace(@_NAME, name) 230 | |> String.replace(@_VERSION, version) 231 | |> String.replace(@_ERTS_VSN, erts) 232 | |> String.replace(@_ERL_OPTS, erl_opts) 233 | File.write!(outfile, contents) 234 | # Make executable 235 | outfile |> chmod(0o700) 236 | end) 237 | 238 | # Continue.. 239 | config 240 | end 241 | 242 | defp execute_before_hooks(%Config{} = config) do 243 | # Just in case there are plugins which expect relx.config to already 244 | # be persisted, we'll persist it before any are run, and again after each plugin runs 245 | Utils.write_terms(relx_config_path(), config.relx_config) 246 | plugins = ReleaseManager.Plugin.load_all 247 | Enum.reduce plugins, config, fn plugin, conf -> 248 | try do 249 | # Handle the case where a child plugin does not return the configuration 250 | config = case plugin.before_release(conf) do 251 | %Config{} = result -> result 252 | _ -> conf 253 | end 254 | Utils.write_terms(relx_config_path(), config.relx_config) 255 | config 256 | rescue 257 | exception -> 258 | stacktrace = System.stacktrace 259 | Logger.error "Failed to execute before_release hook for #{plugin}!" 260 | reraise exception, stacktrace 261 | end 262 | end 263 | end 264 | 265 | defp execute_after_hooks(%Config{} = config) do 266 | plugins = ReleaseManager.Plugin.load_all 267 | Enum.reduce plugins, config, fn plugin, conf -> 268 | try do 269 | # Handle the case where a child plugin does not return the configuration 270 | case plugin.after_release(conf) do 271 | %Config{} = result -> result 272 | _ -> conf 273 | end 274 | rescue 275 | exception -> 276 | stacktrace = System.stacktrace 277 | Logger.error "Failed to execute after_release hook for #{plugin}!" 278 | reraise exception, stacktrace 279 | end 280 | end 281 | end 282 | 283 | defp execute_package_hooks(%Config{} = config) do 284 | plugins = ReleaseManager.Plugin.load_all 285 | Enum.reduce plugins, config, fn plugin, conf -> 286 | try do 287 | # Handle the case where a child plugin does not return the configuration 288 | case plugin.after_package(conf) do 289 | %Config{} = result -> result 290 | _ -> conf 291 | end 292 | rescue 293 | exception -> 294 | stacktrace = System.stacktrace 295 | Logger.error "Failed to execute after_package hook for #{plugin}!" 296 | reraise exception, stacktrace 297 | end 298 | end 299 | end 300 | 301 | defp do_release(%Config{name: name, version: version, verbosity: verbosity, upgrade?: upgrade?, dev: dev_mode?, env: env} = config) do 302 | Logger.debug "Generating release..." 303 | # Persist relx.config one last time in case it was updated by a plugin 304 | Utils.write_terms(relx_config_path(), config.relx_config) 305 | # If this is an upgrade release, generate an appup 306 | if upgrade? do 307 | # Change mix env for appup generation 308 | with_env env, fn -> 309 | # Generate appup 310 | app = name |> String.to_atom 311 | v1 = get_last_release(name) 312 | v1_path = rel_dest_path [name, "lib", "#{name}-#{v1}"] 313 | v2_path = Mix.Project.compile_path |> String.replace("/ebin", "") 314 | own_path = rel_dest_path "#{name}.appup" 315 | # Look for user's own .appup file before generating one 316 | case own_path |> File.exists? do 317 | true -> 318 | # Copy it to ebin 319 | case File.cp(own_path, Path.join([v2_path, "/ebin", "#{name}.appup"])) do 320 | :ok -> 321 | Logger.info "Using custom .appup located in rel/#{name}.appup" 322 | {:error, reason} -> 323 | Logger.error "Unable to copy custom .appup file: #{reason}" 324 | abort!() 325 | end 326 | _ -> 327 | # No custom .appup found, proceed with autogeneration 328 | case ReleaseManager.Appups.make(app, v1, version, v1_path, v2_path) do 329 | {:ok, _} -> 330 | Logger.info "Generated .appup for #{name} #{v1} -> #{version}" 331 | {:error, reason} -> 332 | Logger.error "Appup generation failed with #{reason}" 333 | abort!() 334 | end 335 | end 336 | end 337 | end 338 | # Do release 339 | try do 340 | logs = capture_io(:stdio, fn -> 341 | result = relx(name, version, :verbose, upgrade?, dev_mode?) 342 | send(self(), result) 343 | end) 344 | receive do 345 | :ok -> 346 | if verbosity == :verbose do 347 | IO.puts(logs) 348 | end 349 | # Clean up template files 350 | Mix.Tasks.Release.Clean.do_cleanup(:relfiles) 351 | # Continue.. 352 | config 353 | {:error, message} -> 354 | IO.puts(logs) 355 | Logger.error "ERROR: #{inspect message}" 356 | abort!() 357 | end 358 | catch 359 | err -> 360 | Logger.error "#{IO.inspect err}" 361 | Logger.error "Failed to build release package! Try running with `--verbosity=verbose` to see debugging info!" 362 | abort!() 363 | end 364 | end 365 | 366 | defp generate_nodetool(%Config{name: name} = config) do 367 | Logger.debug "Generating nodetool..." 368 | nodetool = rel_file_source_path @_NODETOOL 369 | dest = rel_dest_path [name, "bin", @_NODETOOL] 370 | # Copy 371 | File.cp! nodetool, dest 372 | # Make executable 373 | dest |> chmod(0o700) 374 | # Continue.. 375 | config 376 | end 377 | 378 | defp generate_install_escript(%Config{name: name} = config) do 379 | escript = rel_file_source_path "install_upgrade.escript" 380 | dest = rel_dest_path [name, "bin", "install_upgrade.escript"] 381 | File.cp! escript, dest 382 | config 383 | end 384 | 385 | defp update_release_package(%Config{dev: true} = config), do: config 386 | defp update_release_package(%Config{name: name, version: version, relx_config: relx_config} = config) do 387 | Logger.debug "Packaging release..." 388 | # Delete original release package 389 | tarball = rel_dest_path [name, "#{name}-#{version}.tar.gz"] 390 | File.rm! tarball 391 | # Make sure we have a start.boot file for upgrades/downgrades 392 | source_boot = rel_dest_path([name, "releases", version, "#{name}.boot"]) 393 | dest_boot = rel_dest_path([name, "releases", version, "start.boot"]) 394 | File.cp! source_boot, dest_boot 395 | # Get include_erts value from relx_config 396 | include_erts = Keyword.get(relx_config, :include_erts, true) 397 | erts = "erts-#{extract_erts_version(config)}" 398 | extras = case include_erts do 399 | false -> [] 400 | _ -> [{'#{erts}', '#{rel_dest_path([name, erts])}'}] 401 | end 402 | # Re-package release with modifications 403 | file_list = File.ls!(rel_dest_path(name)) 404 | |> Enum.reject(fn n -> n in [erts, "tmp"] end) 405 | |> Enum.map(fn 406 | "releases" -> [Path.join("releases", "RELEASES"), 407 | Path.join("releases", "start_erl.data") | 408 | File.ls!(rel_dest_path([name, "releases", version])) 409 | |> Enum.reject(&(String.ends_with?(&1, ".tar.gz"))) 410 | |> Enum.map(fn n -> Path.join(["releases", version, n]) end)] 411 | "lib" -> File.ls!(rel_dest_path([name, "lib"])) 412 | |> Enum.reject(fn n -> String.starts_with?(n, "#{name}-") && !String.ends_with?(n, "-#{version}") end) 413 | |> Enum.map(fn n -> Path.join("lib", n) end) 414 | n -> [n] 415 | end) 416 | |> List.flatten 417 | |> Enum.map(&({'#{&1}', '#{rel_dest_path([name, &1])}'})) 418 | |> Enum.concat(extras) 419 | 420 | # Create archive 421 | release_tarball = rel_dest_path([name, "releases", version, "#{name}.tar.gz"]) 422 | :ok = :erl_tar.create( 423 | '#{tarball}', 424 | file_list, 425 | [:compressed] 426 | ) 427 | # Clean up 428 | File.cp! tarball, release_tarball 429 | File.rm_rf! tarball 430 | 431 | # Continue.. 432 | %{config | package: release_tarball} 433 | end 434 | 435 | defp parse_args(argv) do 436 | {args, _, _} = OptionParser.parse(argv) 437 | defaults = %Config{ 438 | name: Mix.Project.config |> Keyword.get(:app) |> Atom.to_string, 439 | version: Mix.Project.config |> Keyword.get(:version), 440 | env: Mix.env 441 | } 442 | Enum.reduce args, defaults, fn arg, config -> 443 | case arg do 444 | {:verbosity, verbosity} -> 445 | verbosity = String.to_atom(verbosity) 446 | Logger.configure(verbosity) 447 | %{config | :verbosity => verbosity} 448 | {key, value} -> 449 | Map.put(config, key, value) 450 | end 451 | end 452 | end 453 | 454 | defp replace_release_info(template, name, version) do 455 | template 456 | |> String.replace(@_NAME, name) 457 | |> String.replace(@_VERSION, version) 458 | end 459 | 460 | defp extract_erts_version(%Config{relx_config: relx_config}) do 461 | include_erts = Keyword.get(relx_config, :include_erts, true) 462 | case include_erts do 463 | true -> :erlang.system_info(:version) |> IO.iodata_to_binary 464 | false -> "" 465 | path -> 466 | case File.ls("#{path}") do 467 | {:error, _} -> "" 468 | {:ok, entries} -> 469 | erts = entries |> Enum.find(fn 470 | <<"erts-", _version::binary>> -> true 471 | _ -> false 472 | end) 473 | case erts do 474 | <<"erts-", version::binary>> -> version 475 | _ -> "" 476 | end 477 | end 478 | end 479 | end 480 | 481 | def relx_config_path do 482 | path = rel_file_dest_path @_RELXCONF 483 | # Ensure destination base path exists 484 | path |> Path.dirname |> File.mkdir_p! 485 | path 486 | end 487 | 488 | end 489 | -------------------------------------------------------------------------------- /lib/mix/tasks/release.plugins.ex: -------------------------------------------------------------------------------- 1 | defmodule Mix.Tasks.Release.Plugins do 2 | @moduledoc """ 3 | View information about active release plugins 4 | 5 | ## Examples 6 | 7 | # View all active plugins 8 | mix release.plugins 9 | 10 | # View detailed info about a plugin, if available 11 | mix release.plugins 12 | 13 | """ 14 | @shortdoc "View information about active release plugins" 15 | 16 | use Mix.Task 17 | alias ReleaseManager.Utils.Logger 18 | import ReleaseManager.Utils 19 | 20 | def run(args) do 21 | args |> parse_args |> do_run 22 | end 23 | 24 | defp do_run([action: :list]) do 25 | case get_plugins() do 26 | [] -> IO.puts "No plugins found!" 27 | plugins -> 28 | for plugin <- plugins do 29 | name = get_name(plugin) 30 | shortdoc = get_shortdoc(plugin) 31 | IO.puts String.ljust(name, 30) <> " # " <> shortdoc 32 | end 33 | end 34 | end 35 | defp do_run([action: :details, plugin: plugin]) do 36 | plugin |> get_plugin |> display_plugin_long 37 | end 38 | 39 | defp get_plugins, do: ReleaseManager.Plugin.load_all 40 | 41 | defp get_plugin(plugin) do 42 | plugin_name = plugin |> String.downcase 43 | result = ReleaseManager.Plugin.load_all |> Enum.find(fn module -> 44 | module_name = module |> Atom.to_string |> String.downcase 45 | given_name = get_name(module) |> String.downcase 46 | cond do 47 | module_name |> String.contains?(plugin_name) -> true 48 | given_name |> String.contains?(plugin_name) -> true 49 | true -> false 50 | end 51 | end) 52 | case result do 53 | nil -> 54 | Logger.notice "No plugin by that name could be found!" 55 | abort!() 56 | _ -> 57 | result 58 | end 59 | end 60 | 61 | defp display_plugin_long(plugin) do 62 | name = get_name(plugin) 63 | moduledoc = get_moduledoc(plugin) 64 | if IO.ANSI.enabled? do 65 | opts = [width: 80] 66 | IO.ANSI.Docs.print_heading("#{name}", opts) 67 | IO.ANSI.Docs.print(moduledoc, opts) 68 | else 69 | IO.puts "# #{name}\n" 70 | IO.puts moduledoc 71 | end 72 | end 73 | 74 | defp get_name(plugin) do 75 | default = plugin |> Atom.to_string |> String.replace(~r/.*\./, "") |> String.downcase 76 | get_plugin_info(plugin, :name, default) 77 | end 78 | defp get_shortdoc(plugin), do: get_plugin_info(plugin, :shortdoc, "No description available.") 79 | defp get_moduledoc(plugin), do: get_plugin_info(plugin, :moduledoc, "No additional details available.") 80 | 81 | defp get_plugin_info(plugin, type, default) when is_atom(plugin) and is_atom(type) do 82 | case plugin.__info__(:attributes) |> List.keyfind(type, 0) do 83 | {^type, [value]} -> value 84 | nil -> default 85 | end 86 | end 87 | 88 | defp parse_args(args) do 89 | case OptionParser.parse(args) do 90 | {_, [], _} -> [action: :list] 91 | {_, [plugin], _} -> [action: :details, plugin: plugin] 92 | {_, _, _} -> 93 | Logger.error "Invalid arguments for `mix release.plugins`!" 94 | abort!() 95 | end 96 | end 97 | 98 | end 99 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule ReleaseManager.Mixfile do 2 | use Mix.Project 3 | 4 | def project do 5 | [ app: :exrm, 6 | version: "1.0.8", 7 | elixir: "~> 1.0", 8 | description: description(), 9 | package: package(), 10 | deps: deps(), 11 | docs: docs(), 12 | test_coverage: [tool: Coverex.Task, coveralls: true]] 13 | end 14 | 15 | def application, do: [ 16 | applications: [:logger, :relx] 17 | ] 18 | 19 | def deps do 20 | [{:relx, "~> 3.5" }, 21 | {:earmark, "~> 1.0", only: :dev}, 22 | {:ex_doc, "~> 0.13", only: :dev}, 23 | {:coverex, "~> 1.4", only: :test}] 24 | end 25 | 26 | defp description do 27 | """ 28 | Exrm, or Elixir Release Manager, provides mix tasks for building, 29 | upgrading, and controlling release packages for your application. 30 | """ 31 | end 32 | 33 | defp package do 34 | [ files: ["lib", "priv", "mix.exs", "README.md", "LICENSE"], 35 | maintainers: ["Paul Schoenfelder"], 36 | licenses: ["MIT"], 37 | links: %{ "GitHub": "https://github.com/bitwalker/exrm" } ] 38 | end 39 | 40 | defp docs do 41 | [main: "getting-started", 42 | extras: [ 43 | "docs/Getting Started.md", 44 | "docs/Release Configuration.md", 45 | "docs/Deployment.md", 46 | "docs/Upgrades and Downgrades.md", 47 | "docs/Common Issues.md", 48 | "docs/Examples.md" 49 | ]] 50 | end 51 | 52 | end 53 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{"bbmustache": {:hex, :bbmustache, "1.0.4", "7ba94f971c5afd7b6617918a4bb74705e36cab36eb84b19b6a1b7ee06427aa38", [:rebar], []}, 2 | "certifi": {:hex, :certifi, "0.4.0", "a7966efb868b179023618d29a407548f70c52466bf1849b9e8ebd0e34b7ea11f", [:rebar3], []}, 3 | "cf": {:hex, :cf, "0.2.1", "69d0b1349fd4d7d4dc55b7f407d29d7a840bf9a1ef5af529f1ebe0ce153fc2ab", [:rebar3], []}, 4 | "coverex": {:hex, :coverex, "1.4.10", "f6b68f95b3d51d04571a09dd2071c980e8398a38cf663db22b903ecad1083d51", [:mix], [{:httpoison, "~> 0.9", [hex: :httpoison, optional: false]}, {:poison, "~> 1.5 or ~> 2.0", [hex: :poison, optional: false]}]}, 5 | "earmark": {:hex, :earmark, "1.0.1", "2c2cd903bfdc3de3f189bd9a8d4569a075b88a8981ded9a0d95672f6e2b63141", [:mix], []}, 6 | "erlware_commons": {:hex, :erlware_commons, "0.21.0", "a04433071ad7d112edefc75ac77719dd3e6753e697ac09428fc83d7564b80b15", [:rebar3], [{:cf, "0.2.1", [hex: :cf, optional: false]}]}, 7 | "ex_doc": {:hex, :ex_doc, "0.13.0", "aa2f8fe4c6136a2f7cfc0a7e06805f82530e91df00e2bff4b4362002b43ada65", [:mix], [{:earmark, "~> 1.0", [hex: :earmark, optional: false]}]}, 8 | "getopt": {:hex, :getopt, "0.8.2", "b17556db683000ba50370b16c0619df1337e7af7ecbf7d64fbf8d1d6bce3109b", [:rebar], []}, 9 | "hackney": {:hex, :hackney, "1.6.1", "ddd22d42db2b50e6a155439c8811b8f6df61a4395de10509714ad2751c6da817", [:rebar3], [{:certifi, "0.4.0", [hex: :certifi, optional: false]}, {:idna, "1.2.0", [hex: :idna, optional: false]}, {:metrics, "1.0.1", [hex: :metrics, optional: false]}, {:mimerl, "1.0.2", [hex: :mimerl, optional: false]}, {:ssl_verify_fun, "1.1.0", [hex: :ssl_verify_fun, optional: false]}]}, 10 | "httpoison": {:hex, :httpoison, "0.9.0", "68187a2daddfabbe7ca8f7d75ef227f89f0e1507f7eecb67e4536b3c516faddb", [:mix], [{:hackney, "~> 1.6.0", [hex: :hackney, optional: false]}]}, 11 | "idna": {:hex, :idna, "1.2.0", "ac62ee99da068f43c50dc69acf700e03a62a348360126260e87f2b54eced86b2", [:rebar3], []}, 12 | "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], []}, 13 | "mimerl": {:hex, :mimerl, "1.0.2", "993f9b0e084083405ed8252b99460c4f0563e41729ab42d9074fd5e52439be88", [:rebar3], []}, 14 | "neotoma": {:hex, :neotoma, "1.7.3"}, 15 | "poison": {:hex, :poison, "2.2.0", "4763b69a8a77bd77d26f477d196428b741261a761257ff1cf92753a0d4d24a63", [:mix], []}, 16 | "providers": {:hex, :providers, "1.6.0", "db0e2f9043ae60c0155205fcd238d68516331d0e5146155e33d1e79dc452964a", [:rebar3], [{:getopt, "0.8.2", [hex: :getopt, optional: false]}]}, 17 | "relx": {:hex, :relx, "3.20.0", "b515b8317d25b3a1508699294c3d1fa6dc0527851dffc87446661bce21a36710", [:rebar3], [{:bbmustache, "1.0.4", [hex: :bbmustache, optional: false]}, {:cf, "0.2.1", [hex: :cf, optional: false]}, {:erlware_commons, "0.21.0", [hex: :erlware_commons, optional: false]}, {:getopt, "0.8.2", [hex: :getopt, optional: false]}, {:providers, "1.6.0", [hex: :providers, optional: false]}]}, 18 | "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.0", "edee20847c42e379bf91261db474ffbe373f8acb56e9079acb6038d4e0bf414f", [:rebar, :make], []}, 19 | "ssl_verify_hostname": {:hex, :ssl_verify_hostname, "1.0.5"}} 20 | -------------------------------------------------------------------------------- /priv/rel/files/boot: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | if [ -n "${EXRM_INIT_TRACE}" ]; then 4 | set -x 5 | fi 6 | 7 | SCRIPT_DIR="$(dirname "$0")" 8 | RELEASE_ROOT_DIR="$(cd "$SCRIPT_DIR/../.." && pwd)" 9 | REL_NAME="{{{PROJECT_NAME}}}" 10 | RELEASES_DIR="$RELEASE_ROOT_DIR/releases" 11 | REL_VSN=$(cat "$RELEASES_DIR"/start_erl.data | cut -d' ' -f2) 12 | ERTS_VSN=$(cat "$RELEASES_DIR"/start_erl.data | cut -d' ' -f1) 13 | REL_DIR="$RELEASES_DIR/$REL_VSN" 14 | REL_LIB_DIR="$RELEASE_ROOT_DIR/lib" 15 | ERL_OPTS="{{{ERL_OPTS}}} ${ERL_OPTS}" 16 | CONFORM_OPTS="" 17 | PIPE_DIR="$RELEASE_ROOT_DIR/tmp/erl_pipes/{{{PROJECT_NAME}}}/" 18 | ERTS_DIR="" 19 | ROOTDIR="" 20 | 21 | GENERATED_CONFIG_DIR="${RELEASE_ROOT_DIR}/running-config" 22 | # Check for $RELEASE_MUTABLE_DIR 23 | if [ -n "${RELEASE_MUTABLE_DIR}" ]; then 24 | PIPE_DIR="${RELEASE_MUTABLE_DIR}/erl_pipes/" 25 | RUNNER_LOG_DIR="${RUNNER_LOG_DIR:-${RELEASE_MUTABLE_DIR}/log}" 26 | GENERATED_CONFIG_DIR="${RELEASE_MUTABLE_DIR}/running-config" 27 | fi 28 | 29 | RUNNER_LOG_DIR="${RUNNER_LOG_DIR:-$RELEASE_ROOT_DIR/log}" 30 | 31 | # Do a textual replacement of ${VAR} occurances in $1 and pipe to $2 32 | _replace_os_vars() { 33 | awk ' 34 | function escape(s) { 35 | gsub(/\\/, "\\\\", s); 36 | gsub(/\&/, "\\\\&", s); 37 | return s; 38 | } 39 | { 40 | while(match($0,"[$]{[^}]*}")) { 41 | var=substr($0,RSTART+2,RLENGTH-3); 42 | gsub("[$]{"var"}", escape(ENVIRON[var])) 43 | } 44 | }1' < "$1" > "$2" 45 | } 46 | 47 | find_erts_dir() { 48 | __erts_dir="$RELEASE_ROOT_DIR/erts-$ERTS_VSN" 49 | if [ -d "$__erts_dir" ]; then 50 | ERTS_DIR="$__erts_dir"; 51 | ROOTDIR="$RELEASE_ROOT_DIR" 52 | else 53 | __erl="$(which erl)" 54 | __code="io:format(\"~s\", [code:root_dir()])." 55 | __erl_root="$("$__erl" -noshell -eval "$__code" -s init stop)" 56 | ERTS_DIR=$(ls -d $__erl_root/erts-* | sort -t '.' -k 1,1 -k 2,2 -k 3,3 -k 4,4 -k 5,5 -g | tail -n 1) 57 | ROOTDIR="$__erl_root" 58 | fi 59 | } 60 | 61 | # Connect to a remote node 62 | relx_rem_sh() { 63 | # Generate a unique id used to allow multiple remsh to the same node 64 | # transparently 65 | id="remsh$(relx_gen_id)-${NAME}" 66 | 67 | # Get the node's ticktime so that we use the same thing 68 | TICKTIME="$(relx_nodetool rpcterms net_kernel get_net_ticktime)" 69 | 70 | # Setup Erlang remote shell command to control node 71 | #exec "$ERTS_DIR/bin/erl" "$NAME_TYPE" "$id" -remsh "$NAME" -boot start_clean \ 72 | #-setcookie "$COOKIE" -kernel net_ticktime "$TICKTIME" 73 | 74 | # Setup Elixir remote shell command to control node 75 | exec "$BINDIR/erl" \ 76 | -pa "$ROOTDIR"/lib/*/ebin -pa "$CONSOLIDATED_DIR" \ 77 | -hidden -noshell \ 78 | -boot start_clean -boot_var ERTS_LIB_DIR "$ERTS_LIB_DIR" \ 79 | -kernel net_ticktime "$TICKTIME" \ 80 | -user Elixir.IEx.CLI "$NAME_TYPE" "$id" -setcookie "$COOKIE" \ 81 | -extra --no-halt +iex -"$NAME_TYPE" "$id" --cookie "$COOKIE" --remsh "$NAME" 82 | } 83 | 84 | # Generate a random id 85 | relx_gen_id() { 86 | od -t x4 /dev/urandom | head -n1 | cut -d ' ' -f2 87 | } 88 | 89 | # Control a node - set PEERNAME to control a peer node 90 | relx_nodetool() { 91 | command="$1"; shift 92 | name=${PEERNAME:-$NAME} 93 | "$BINDIR/escript" "$ROOTDIR/bin/nodetool" "$NAME_TYPE" "$name" \ 94 | -setcookie "$COOKIE" "$command" "$@" 95 | } 96 | 97 | # Run an escript in the node's environment 98 | relx_escript() { 99 | shift; __scriptpath="$1"; shift 100 | export RELEASE_ROOT_DIR 101 | "$BINDIR/escript" "$ROOTDIR/$__scriptpath" $@ 102 | } 103 | 104 | # Output a start command for the last argument of run_erl 105 | relx_start_command() { 106 | printf "exec \"%s\" \"%s\"" "$RELEASE_ROOT_DIR/bin/$REL_NAME" \ 107 | "$START_OPTION" 108 | } 109 | 110 | # Convert .conf to sys.config using conform escript 111 | generate_config() { 112 | __schema_file="$REL_DIR/$REL_NAME.schema.exs" 113 | if [ -z "$RELEASE_CONFIG_FILE" ]; then 114 | __conform_file="$RELEASE_CONFIG_DIR/$REL_NAME.conf" 115 | else 116 | if [ -r "$RELEASE_CONFIG_FILE" ]; then 117 | __conform_file="$RELEASE_CONFIG_FILE" 118 | else 119 | echo "$RELEASE_CONFIG_FILE not found" 120 | exit 1 121 | fi 122 | fi 123 | if [ -f "$__schema_file" ]; then 124 | if [ -f "$__conform_file" ]; then 125 | __running_conf="$GENERATED_CONFIG_DIR/$REL_NAME.conf" 126 | CONFORM_OPTS="-conform_schema ${__schema_file} -conform_config ${__conform_file} -running_conf ${__running_conf}" 127 | 128 | # always copy release-config to running-config 129 | echo "copying $__conform_file to $__running_conf ..." 130 | cp "$__conform_file" "$__running_conf" 131 | __conform_file="$__running_conf" 132 | 133 | echo "using $__conform_file to populate \"$GENERATED_CONFIG_DIR\"." 134 | __conform="$REL_DIR/conform" 135 | # Handle the case where the current version did not bundle conform in the release 136 | if [ ! -f "$__conform" ]; then 137 | __conform="$ROOTDIR/bin/conform" 138 | fi 139 | result="$("$BINDIR/escript" "$__conform" --conf "$__conform_file" --schema "$__schema_file" --config "$RELEASE_SYS_CONFIG" --output-dir "$GENERATED_CONFIG_DIR")" 140 | exit_status="$?" 141 | if [ "$exit_status" -ne 0 ]; then 142 | exit "$exit_status" 143 | fi 144 | if [ ! -r "${GENERATED_CONFIG_DIR}/sys.config" ]; then 145 | echo "conform succeeded, but not sys.config generated at \"${GENERATED_CONFIG_DIR}/sys.config\"." 146 | exit 1 147 | fi 148 | else 149 | echo "$__conform_file not found in $RELEASE_CONFIG_DIR" 150 | exit 1 151 | fi 152 | else 153 | cp $RELEASE_SYS_CONFIG "$GENERATED_CONFIG_DIR/sys.config" 154 | fi 155 | if [ ! -f "$VMARGS_PATH" ]; then 156 | cp "$RELEASE_CONFIG_DIR/vm.args" "$GENERATED_CONFIG_DIR/vm.args" 157 | VMARGS_PATH="$GENERATED_CONFIG_DIR/vm.args" 158 | fi 159 | } 160 | 161 | # Make directory for mutable configs exists 162 | mkdir -pv "$GENERATED_CONFIG_DIR" 163 | 164 | # Use configs from environment if defined, otherwise releases/VSN 165 | if [ -z "$RELEASE_CONFIG_DIR" ]; then 166 | RELEASE_CONFIG_DIR=$REL_DIR 167 | fi 168 | 169 | SYS_CONFIG="$GENERATED_CONFIG_DIR/sys.config" 170 | RELEASE_SYS_CONFIG="$RELEASE_CONFIG_DIR/sys.config" 171 | if [ -z "$VMARGS_PATH" ]; then 172 | VMARGS_PATH="$GENERATED_CONFIG_DIR/vm.args" 173 | fi 174 | 175 | # If first run, take dafault sys.config and vm.args 176 | if [ ! -f "$SYS_CONFIG" ]; then 177 | cp $RELEASE_SYS_CONFIG "$GENERATED_CONFIG_DIR/sys.config" 178 | fi 179 | if [ ! -f "$VMARGS_PATH" ]; then 180 | cp "$RELEASE_CONFIG_DIR/vm.args" "$GENERATED_CONFIG_DIR/vm.args" 181 | fi 182 | 183 | if [ $RELX_REPLACE_OS_VARS ]; then 184 | _replace_os_vars "$VMARGS_PATH" "$VMARGS_PATH.2.config" 185 | VMARGS_PATH=$VMARGS_PATH.2.config 186 | fi 187 | 188 | # Make sure log directory exists 189 | mkdir -p "$RUNNER_LOG_DIR" 190 | 191 | # Make sure the current user has write permission 192 | if ! [ -w $RUNNER_LOG_DIR ] ; then 193 | echo "Unable to write to $RUNNER_LOG_DIR. Quitting." 194 | exit 1 195 | fi 196 | 197 | if [ $RELX_REPLACE_OS_VARS ]; then 198 | _replace_os_vars "$SYS_CONFIG" "$SYS_CONFIG.2.config" 199 | SYS_CONFIG=$SYS_CONFIG.2.config 200 | fi 201 | 202 | # Extract the target node name from node.args 203 | NAME_ARG=$(egrep '^-s?name' "$VMARGS_PATH") 204 | if [ -z "$NAME_ARG" ]; then 205 | echo "vm.args needs to have either -name or -sname parameter." 206 | exit 1 207 | fi 208 | 209 | # Extract the name type and name from the NAME_ARG for REMSH 210 | NAME_TYPE="$(echo "$NAME_ARG" | awk '{print $1}')" 211 | NAME="$(echo "$NAME_ARG" | awk '{print $2}')" 212 | 213 | # User can specify an sname without @hostname 214 | # This will fail when creating remote shell 215 | # So here we check for @ and add @hostname if missing 216 | case $NAME in 217 | *@*) 218 | # Nothing to do 219 | ;; 220 | *) 221 | # Add @hostname 222 | case $NAME_TYPE in 223 | -sname) 224 | NAME=$NAME@`hostname -s` 225 | ;; 226 | -name) 227 | NAME=$NAME@`hostname -f` 228 | ;; 229 | esac 230 | ;; 231 | esac 232 | 233 | PIPE_DIR="${PIPE_DIR:-/tmp/erl_pipes/$NAME/}" 234 | 235 | # Extract the target cookie 236 | COOKIE_ARG="$(grep '^-setcookie' "$VMARGS_PATH")" 237 | if [ -z "$COOKIE_ARG" ]; then 238 | echo "vm.args needs to have a -setcookie parameter." 239 | exit 1 240 | fi 241 | 242 | # Extract cookie name from COOKIE_ARG 243 | COOKIE="$(echo "$COOKIE_ARG" | awk '{print $2}')" 244 | 245 | find_erts_dir 246 | export ROOTDIR="$RELEASE_ROOT_DIR" 247 | export BINDIR="$ERTS_DIR/bin" 248 | export EMU="beam" 249 | export PROGNAME="erl" 250 | export LD_LIBRARY_PATH="$ERTS_DIR/lib:$LD_LIBRARY_PATH" 251 | ERTS_LIB_DIR="$ERTS_DIR/../lib" 252 | CONSOLIDATED_DIR="$ROOTDIR/lib/${REL_NAME}-${REL_VSN}/consolidated" 253 | 254 | cd "$ROOTDIR" 255 | 256 | # Check the first argument for instructions 257 | case "$1" in 258 | start|start_boot) 259 | # Make sure the config is generated first 260 | generate_config 261 | # Make sure there is not already a node running 262 | #RES=`$NODETOOL ping` 263 | #if [ "$RES" = "pong" ]; then 264 | # echo "Node is already running!" 265 | # exit 1 266 | #fi 267 | # Save this for later 268 | CMD="$1" 269 | case "$1" in 270 | start) 271 | shift 272 | START_OPTION="console" 273 | HEART_OPTION="start" 274 | ;; 275 | start_boot) 276 | shift 277 | START_OPTION="console_boot" 278 | HEART_OPTION="start_boot" 279 | ;; 280 | esac 281 | RUN_PARAM="$@" 282 | 283 | # Set arguments for the heart command 284 | set -- "$SCRIPT_DIR/$REL_NAME" "$HEART_OPTION" 285 | [ "$RUN_PARAM" ] && set -- "$@" "$RUN_PARAM" 286 | 287 | # Export the HEART_COMMAND 288 | HEART_COMMAND="$RELEASE_ROOT_DIR/bin/$REL_NAME $CMD" 289 | export HEART_COMMAND 290 | 291 | mkdir -p "$PIPE_DIR" 292 | 293 | # Make sure the current user has write permission 294 | if ! [ -w $PIPE_DIR ] ; then 295 | echo "Unable to write to $PIPE_DIR. Quitting." 296 | exit 1 297 | fi 298 | 299 | "$BINDIR/run_erl" -daemon "$PIPE_DIR" "$RUNNER_LOG_DIR" \ 300 | "$(relx_start_command)" 301 | ;; 302 | 303 | stop) 304 | # Wait for the node to completely stop... 305 | case $(uname -s) in 306 | Linux|Darwin|FreeBSD|DragonFly|NetBSD|OpenBSD) 307 | # PID COMMAND 308 | PID=$(ps ax -o pid= -o command=| 309 | grep "$RELEASE_ROOT_DIR/.*/[b]eam"|awk '{print $1}') 310 | ;; 311 | SunOS) 312 | # PID COMMAND 313 | PID=$(ps -ef -o pid= -o args=| 314 | grep "$RELEASE_ROOT_DIR/.*/[b]eam"|awk '{print $1}') 315 | ;; 316 | CYGWIN*) 317 | # UID PID PPID TTY STIME COMMAND 318 | PID=$(ps -efw|grep "$RELEASE_ROOT_DIR/.*/[b]eam"|awk '{print $2}') 319 | ;; 320 | esac 321 | relx_nodetool "stop" 322 | exit_status=$? 323 | if [ "$exit_status" -ne 0 ]; then 324 | exit $exit_status 325 | fi 326 | # ensuring PID is not empty 327 | if [ -z "$PID" ]; then 328 | exit 0 329 | fi 330 | while $(kill -0 "$PID" 2>/dev/null); 331 | do 332 | sleep 1 333 | done 334 | ;; 335 | 336 | restart) 337 | # Make sure the config is generated first 338 | generate_config 339 | ## Restart the VM without exiting the process 340 | relx_nodetool "restart" 341 | exit_status=$? 342 | if [ "$exit_status" -ne 0 ]; then 343 | exit $exit_status 344 | fi 345 | ;; 346 | 347 | reboot) 348 | # Make sure the config is generated first 349 | generate_config 350 | ## Restart the VM completely (uses heart to restart it) 351 | relx_nodetool "reboot" 352 | exit_status=$? 353 | if [ "$exit_status" -ne 0 ]; then 354 | exit $exit_status 355 | fi 356 | ;; 357 | 358 | ping) 359 | ## See if the VM is alive 360 | relx_nodetool "ping" 361 | exit_status=$? 362 | if [ "$exit_status" -ne 0 ]; then 363 | exit $exit_status 364 | fi 365 | ;; 366 | 367 | pingpeer) 368 | PEERNAME=$2 relx_nodetool "ping" 369 | exit_status=$? 370 | if [ "$exit_status" -ne 0 ]; then 371 | exit $exit_status 372 | fi 373 | ;; 374 | 375 | escript) 376 | # Make sure the config is generated first 377 | generate_config 378 | ## Run an escript under the node's environment 379 | relx_escript $@ 380 | exit_status=$? 381 | if [ "$exit_status" -ne 0 ]; then 382 | exit $exit_status 383 | fi 384 | ;; 385 | 386 | rpc) 387 | ## Execute a command in MFA format on the remote node 388 | if [ -z "$3" ]; then 389 | echo "RPC requires module, function, and a string of the arguments to be evaluated." 390 | echo "The argument string must evaluate to a valid Erlang term." 391 | echo "Examples: rpc calendar valid_date \"{2013,3,12}.\"" 392 | echo " rpc erlang now" 393 | exit 1 394 | fi 395 | if [ -z "$4" ]; then 396 | relx_nodetool "rpcterms" "$2" "$3" 397 | else 398 | module="$2" 399 | function="$3" 400 | shift 3 401 | args=$@ 402 | relx_nodetool "rpcterms" "$module" "$function" "$args" 403 | fi 404 | exit_status="$?" 405 | if [ "$exit_status" -ne 0 ]; then 406 | exit $exit_status 407 | fi 408 | ;; 409 | 410 | attach) 411 | # Make sure a node IS running 412 | relx_nodetool "ping" > /dev/null 413 | exit_status=$? 414 | if [ "$exit_status" -ne 0 ]; then 415 | echo "Node is not running!" 416 | exit $exit_status 417 | fi 418 | 419 | shift 420 | exec "$BINDIR/to_erl" "$PIPE_DIR" 421 | ;; 422 | 423 | remote_console) 424 | # Make sure a node IS running 425 | relx_nodetool "ping" > /dev/null 426 | exit_status=$? 427 | if [ "$exit_status" -ne 0 ]; then 428 | echo "Node is not running!" 429 | exit $exit_status 430 | fi 431 | 432 | shift 433 | relx_rem_sh 434 | ;; 435 | 436 | upgrade|downgrade|install) 437 | if [ -z "$2" ]; then 438 | echo "Missing package argument" 439 | echo "Usage: $REL_NAME $1 {package base name}" 440 | echo "NOTE {package base name} MUST NOT include the .tar.gz suffix" 441 | exit 1 442 | fi 443 | 444 | # Make sure a node IS running 445 | relx_nodetool "ping" > /dev/null 446 | exit_status=$? 447 | if [ "$exit_status" -ne 0 ]; then 448 | echo "Node is not running!" 449 | exit $exit_status 450 | fi 451 | 452 | # We have to unpack the release first in order to make sure the configuration 453 | # is properly updated. This also destroys the .tar.gz package (release_handler does) 454 | "$BINDIR/escript" "$ROOTDIR/bin/install_upgrade.escript" \ 455 | "unpack" "$REL_NAME" "$NAME_TYPE" "$NAME" "$COOKIE" "$2" 456 | 457 | echo "Generating vm.args/sys.config for upgrade..." 458 | __release_conf="$RELEASES_DIR/$2/$REL_NAME.conf" 459 | __release_schema="$RELEASES_DIR/$2/$REL_NAME.schema.exs" 460 | __release_config="$RELEASES_DIR/$2/sys.config" 461 | __release_args="$RELEASES_DIR/$2/vm.args" 462 | __running_conf="$GENERATED_CONFIG_DIR/$REL_NAME.conf" 463 | __running_config="$GENERATED_CONFIG_DIR/sys.config" 464 | __running_args="$GENERATED_CONFIG_DIR/vm.args" 465 | # Make sure the .conf is copied to the running-config directory 466 | if [ -f "$__release_conf" ]; then 467 | # Preserve previous conf for reference 468 | if [ -f "$__running_conf" ]; then 469 | cp "$__running_conf" "$__running_conf.last" 470 | fi 471 | cp "$__release_conf" "$__running_conf" 472 | fi 473 | __conform="$RELEASES_DIR/$2/conform" 474 | # Handle the case where the target version did not bundle conform in the release 475 | if [ ! -f "$__conform" ]; then 476 | __conform="$ROOTDIR/bin/conform" 477 | fi 478 | # Generate the sys.config for the release 479 | # If a .conf is not provided, then preserve the last sys.config for reference, and copy the new sys.config 480 | # to running-config. 481 | if [ -f "$__running_conf" ]; then 482 | if [ -f "$__release_schema" ]; then 483 | result="$("$BINDIR/escript" "$__conform" --conf "$__running_conf" --schema "$__release_schema" --config "$__release_config" --output-dir "$RELEASES_DIR/$2")" 484 | exit_status="$?" 485 | if [ "$exit_status" -ne 0 ]; then 486 | echo "Could not generate the sys.config for the new release. Please review the following files:" 487 | echo "$REL_NAME.conf: $__running_conf" 488 | echo "$REL_NAME.schema.exs: $__release_schema" 489 | echo "sys.config: $__release_config" 490 | exit "$exit_status" 491 | else 492 | cp "$__release_config" "$__running_config" 493 | fi 494 | fi 495 | else 496 | if [ -f "$__running_config" ]; then 497 | cp "$__running_config" "$__running_config.last" 498 | cp "$__release_config" "$__running_config" 499 | fi 500 | fi 501 | echo "sys.config ready!" 502 | if [ -f "$__running_args" ]; then 503 | cp "$__release_args" "$__release_args.orig" 504 | cp "$__running_args" "$__release_args" 505 | else 506 | cp "$__release_args" "$__running_args" 507 | fi 508 | echo "vm.args ready!" 509 | 510 | exec "$BINDIR/escript" "$ROOTDIR/bin/install_upgrade.escript" \ 511 | "install" "$REL_NAME" "$NAME_TYPE" "$NAME" "$COOKIE" "$2" 512 | ;; 513 | 514 | unpack) 515 | if [ -z "$2" ]; then 516 | echo "Missing package argument" 517 | echo "Usage: $REL_NAME $1 {package base name}" 518 | echo "NOTE {package base name} MUST NOT include the .tar.gz suffix" 519 | exit 1 520 | fi 521 | 522 | # Make sure a node IS running 523 | if ! relx_nodetool "ping" > /dev/null; then 524 | echo "Node is not running!" 525 | exit 1 526 | fi 527 | 528 | exec "$BINDIR/escript" "$ROOTDIR/bin/install_upgrade.escript" \ 529 | "unpack" "$REL_NAME" "$NAME_TYPE" "$NAME" "$COOKIE" "$2" 530 | ;; 531 | 532 | console|console_clean|console_boot) 533 | # Make sure the config is generated first 534 | generate_config 535 | # .boot file typically just $REL_NAME (ie, the app name) 536 | # however, for debugging, sometimes start_clean.boot is useful. 537 | # For e.g. 'setup', one may even want to name another boot script. 538 | __console_flags="" 539 | case "$1" in 540 | console) 541 | __console_flags="-mode embedded" 542 | if [ -f "$REL_DIR/$REL_NAME.boot" ]; then 543 | BOOTFILE="$REL_DIR/$REL_NAME" 544 | else 545 | BOOTFILE="$REL_DIR/start" 546 | fi 547 | ;; 548 | console_clean) 549 | BOOTFILE="$ROOTDIR/bin/start_clean" 550 | ;; 551 | console_boot) 552 | shift 553 | BOOTFILE="$1" 554 | shift 555 | ;; 556 | esac 557 | # Setup beam-required vars 558 | EMU="beam" 559 | PROGNAME="${0#*/}" 560 | 561 | export EMU 562 | export PROGNAME 563 | 564 | # Store passed arguments since they will be erased by `set` 565 | ARGS="$@" 566 | 567 | # Build an array of arguments to pass to exec later on 568 | # Build it here because this command will be used for logging. 569 | set -- "$BINDIR/erlexec" \ 570 | -boot "$BOOTFILE" -config "$SYS_CONFIG" \ 571 | -boot_var ERTS_LIB_DIR "$ERTS_LIB_DIR" \ 572 | -env ERL_LIBS "$REL_LIB_DIR" \ 573 | -pa "$CONSOLIDATED_DIR" \ 574 | -args_file "$VMARGS_PATH" \ 575 | ${__console_flags} \ 576 | ${ERL_OPTS} \ 577 | ${CONFORM_OPTS} \ 578 | -user Elixir.IEx.CLI -extra --no-halt +iex 579 | 580 | # Dump environment info for logging purposes 581 | echo "Exec: $@" -- ${1+$ARGS} 582 | echo "Root: $ROOTDIR" 583 | 584 | # Log the startup 585 | echo "$RELEASE_ROOT_DIR" 586 | logger -t "$REL_NAME[$$]" "Starting up" 587 | 588 | # Start the VM 589 | exec "$@" -- ${1+$ARGS} 590 | ;; 591 | 592 | foreground) 593 | # Make sure the config is generated first 594 | generate_config 595 | # start up the release in the foreground for use by runit 596 | # or other supervision services 597 | 598 | [ -f "$REL_DIR/$REL_NAME.boot" ] && BOOTFILE="$REL_NAME" || BOOTFILE="start" 599 | FOREGROUNDOPTIONS="-noshell -noinput +Bd" 600 | 601 | # Setup beam-required vars 602 | EMU="beam" 603 | PROGNAME="${0#*/}" 604 | 605 | export EMU 606 | export PROGNAME 607 | 608 | # Store passed arguments since they will be erased by `set` 609 | ARGS="$@" 610 | 611 | # Build an array of arguments to pass to exec later on 612 | # Build it here because this command will be used for logging. 613 | set -- "$BINDIR/erlexec" $FOREGROUNDOPTIONS \ 614 | -boot "$REL_DIR/$BOOTFILE" -mode embedded -config "$SYS_CONFIG" \ 615 | -boot_var ERTS_LIB_DIR "$ERTS_LIB_DIR" \ 616 | -env ERL_LIBS "$REL_LIB_DIR" \ 617 | -pa "$CONSOLIDATED_DIR" \ 618 | ${ERL_OPTS} \ 619 | ${CONFORM_OPTS} \ 620 | -args_file "$VMARGS_PATH" 621 | 622 | # Dump environment info for logging purposes 623 | echo "Exec: $@" -- ${1+$ARGS} 624 | echo "Root: $ROOTDIR" 625 | 626 | # Start the VM 627 | exec "$@" -- ${1+$ARGS} 628 | ;; 629 | 630 | command) 631 | # Make sure the config is generated first 632 | generate_config 633 | 634 | # Execute as command-line utility 635 | # 636 | # Like the escript command, this does not start the OTP application. 637 | # If your command depends on a running OTP application, 638 | # use 639 | # 640 | # {:ok, _} = Application.ensure_all_started(:your_app) 641 | 642 | shift 643 | MODULE="$1"; shift 644 | FUNCTION="$1"; shift 645 | 646 | # Save extra arguments 647 | ARGS="$@" 648 | 649 | # Build arguments for erlexec 650 | set -- "$ERL_OPTS" 651 | [ "$SYS_CONFIG" ] && set -- "$@" -config "$SYS_CONFIG" 652 | set -- "$@" -boot_var ERTS_LIB_DIR "$ERTS_LIB_DIR" 653 | set -- "$@" -noshell 654 | set -- "$@" -boot $REL_DIR/start_clean 655 | set -- "$@" -s "$MODULE" "$FUNCTION" 656 | 657 | # Boot the release 658 | $BINDIR/erlexec $@ -extra $ARGS 659 | exit "$?" 660 | ;; 661 | 662 | *) 663 | echo "Usage: $REL_NAME {start|start_boot |foreground|stop|restart|reboot|ping|rpc []|console|console_clean|console_boot |attach|remote_console|upgrade|escript|command }" 664 | exit 1 665 | ;; 666 | esac 667 | 668 | exit 0 669 | -------------------------------------------------------------------------------- /priv/rel/files/boot.bat: -------------------------------------------------------------------------------- 1 | :: This batch file handles managing an Erlang node as a Windows service. 2 | :: 3 | :: Commands provided: 4 | :: 5 | :: * install - install the release as a Windows service 6 | :: * start - start the service and Erlang node 7 | :: * stop - stop the service and Erlang node 8 | :: * restart - run the stop command and start command 9 | :: * uninstall - uninstall the service and kill a running node 10 | :: * ping - check if the node is running 11 | :: * console - start the Erlang release in a `werl` Windows shell 12 | :: * attach - connect to a running node and open an interactive console 13 | :: * list - display a listing of installed Erlang services 14 | :: * usage - display available commands 15 | 16 | @if defined ELIXIR_CLI_ECHO (@echo on) else (@echo off) 17 | 18 | :: Set variables that describe the release 19 | @set rel_name={{{PROJECT_NAME}}} 20 | @set erl_opts={{{ERL_OPTS}}} 21 | @set conform_opts= 22 | 23 | :: Discover the release root directory from the directory of this script 24 | @set script_dir=%~dp0 25 | @for %%A in ("%script_dir%\..\..") do @( 26 | set release_root_dir=%%~fA 27 | ) 28 | @set start_erl=%release_root_dir%\releases\start_erl.data 29 | @for /f "delims=" %%i in ('type %start_erl%') do @( 30 | set start_erl_data=%%i 31 | ) 32 | @for /f "tokens=1,* delims=\ " %%a in ("%start_erl_data%") do @( 33 | set erts_vsn=%%a 34 | set rel_vsn=%%b 35 | ) 36 | @set rel_dir=%release_root_dir%\releases\%rel_vsn% 37 | 38 | @call :find_erts_dir 39 | @call :find_sys_config 40 | @call :set_boot_script_var 41 | 42 | @set service_name=%rel_name%_%rel_vsn% 43 | @set bindir=%erts_dir%\bin 44 | @set vm_args=%rel_dir%\vm.args 45 | @set progname=erl.exe 46 | @set clean_boot_script=%release_root_dir%\bin\start_clean 47 | @set erlsrv=%bindir%\erlsrv.exe 48 | @set epmd=%bindir%\epmd.exe 49 | @set escript=%bindir%\escript.exe 50 | @set werl=%bindir%\werl.exe 51 | @set nodetool=%release_root_dir%\bin\nodetool 52 | @set conform=%rel_dir%\conform 53 | 54 | :: Extract node type and name from vm.args 55 | @for /f "usebackq tokens=1-2" %%I in (`findstr /b "\-name \-sname" "%vm_args%"`) do @( 56 | set node_type=%%I 57 | set node_name=%%J 58 | ) 59 | 60 | :: Extract cookie from vm.args 61 | @for /f "usebackq tokens=1-2" %%I in (`findstr /b \-setcookie "%vm_args%"`) do @( 62 | set cookie=%%J 63 | ) 64 | 65 | :: Write the erl.ini file to set up paths relative to this script 66 | @call :write_ini 67 | 68 | :: If a start.boot file is not present, copy one from the named .boot file 69 | @if not exist "%rel_dir%\start.boot" ( 70 | copy "%rel_dir%\%rel_name%.boot" "%rel_dir%\start.boot" >nul 71 | ) 72 | 73 | @if "%1"=="install" @goto install 74 | @if "%1"=="uninstall" @goto uninstall 75 | @if "%1"=="start" @goto start 76 | @if "%1"=="stop" @goto stop 77 | @if "%1"=="restart" @call :stop && @goto start 78 | @if "%1"=="upgrade" @goto relup 79 | @if "%1"=="downgrade" @goto relup 80 | @if "%1"=="console" @goto console 81 | @if "%1"=="ping" @goto ping 82 | @if "%1"=="list" @goto list 83 | @if "%1"=="attach" @goto attach 84 | @if "%1"=="" @goto usage 85 | @echo Unknown command: "%1" 86 | 87 | @goto :eof 88 | 89 | :: Find the ERTS dir 90 | :find_erts_dir 91 | @set possible_erts_dir=%release_root_dir%\erts-%erts_vsn% 92 | @if exist "%possible_erts_dir%" ( 93 | call :set_erts_dir_from_default 94 | ) else ( 95 | call :set_erts_dir_from_erl 96 | ) 97 | @goto :eof 98 | 99 | :: Set the ERTS dir from the passed in erts_vsn 100 | :set_erts_dir_from_default 101 | @set erts_dir=%possible_erts_dir% 102 | @for %%e in ("%erts_dir%") do set erts_dir=%%~se 103 | @set rootdir=%release_root_dir% 104 | @for %%r in ("%rootdir%") do set rootdir=%%~sr 105 | @goto :eof 106 | 107 | :: Set the ERTS dir from erl 108 | :set_erts_dir_from_erl 109 | @for /f "delims=" %%i in ('where erl') do @( 110 | set erl=%%~si 111 | ) 112 | @set dir_cmd="%erl%" -noshell -eval "io:format(\"~s\", [filename:nativename(code:root_dir())])." -s init stop 113 | %dir_cmd% > %TEMP%/erlroot.txt 114 | @set /P erl_root=< %TEMP%/erlroot.txt 115 | @for %%f in ("%erl_root%") do set erl_root=%%~sf 116 | @set erts_dir=%erl_root%\erts-%erts_vsn% 117 | @for %%e in ("%erts_dir%") do set erts_dir=%%~se 118 | @set rootdir=%erl_root% 119 | @goto :eof 120 | 121 | :: Find the sys.config file 122 | :find_sys_config 123 | @set possible_sys=%rel_dir%\sys.config 124 | @if exist %possible_sys% ( 125 | set sys_config=%possible_sys% 126 | ) 127 | @goto :eof 128 | 129 | :generate_config 130 | @set conform_schema="%rel_dir%\%rel_name%.schema.exs" 131 | @if "%RELEASE_CONFIG_FILE%"=="" ( 132 | set conform_conf="%rel_dir%\%rel_name%.conf" 133 | ) else ( 134 | if exist "%RELEASE_CONFIG_FILE%" ( 135 | set conform_conf="%RELEASE_CONFIG_FILE%" 136 | ) else ( 137 | echo "RELEASE_CONFIG_FILE not found" 138 | set ERRORLEVEL=1 139 | exit /b %ERRORLEVEL% 140 | goto :eof 141 | ) 142 | ) 143 | @if exist "%conform_schema%" ( 144 | if exist "%conform_conf%" ( 145 | set conform_opts="-conform_schema %conform_schema% -conform_config %conform_conf%" 146 | "%escript%" "%conform%" --conf "%conform_conf%" --schema "%conform_schema%" --config "%sys_config%" --output-dir "%rel_dir%" 147 | if 1==%ERRORLEVEL% ( 148 | exit /b %ERRORLEVEL% 149 | ) 150 | ) else ( 151 | goto :eof 152 | ) 153 | ) 154 | @goto :eof 155 | 156 | :: set boot_script variable 157 | :set_boot_script_var 158 | @if exist "%rel_dir%\%rel_name%.boot" ( 159 | set boot_script=%rel_dir%\%rel_name% 160 | ) else ( 161 | set boot_script=%rel_dir%\start 162 | ) 163 | @goto :eof 164 | 165 | :: Write the erl.ini file 166 | :write_ini 167 | @set erl_ini=%erts_dir%\bin\erl.ini 168 | @set converted_bindir=%bindir:\=\\% 169 | @set converted_rootdir=%rootdir:\=\\% 170 | @echo [erlang] > "%erl_ini%" 171 | @echo Bindir=%converted_bindir% >> "%erl_ini%" 172 | @echo Progname=%progname% >> "%erl_ini%" 173 | @echo Rootdir=%converted_rootdir% >> "%erl_ini%" 174 | @goto :eof 175 | 176 | :: Display usage information 177 | :usage 178 | @echo usage: %~n0 ^(install^|uninstall^|start^|stop^|restart^|upgrade^|downgrade^|console^|ping^|list^|attach^) 179 | @goto :eof 180 | 181 | :: Install the release as a Windows service 182 | :: or install the specified version passed as argument 183 | :install 184 | @if "" == "%2" ( 185 | :: Install the service 186 | set args=%erl_opts% %conform_opts% -setcookie %cookie% ++ -rootdir %rootdir% 187 | set svc_machine=%erts_dir%\bin\start_erl.exe 188 | set description=Erlang node %node_name% in %rootdir% 189 | %erlsrv% add %service_name% %node_type% "%node_name%" -c "%description%" ^ 190 | -w "%rootdir%" -m "%svc_machine%" -args "%args%" ^ 191 | -stopaction "init:stop()." 192 | ) else ( 193 | :: relup and reldown 194 | goto relup 195 | ) 196 | @goto :eof 197 | 198 | :: Uninstall the Windows service 199 | :uninstall 200 | @%erlsrv% remove %service_name% 201 | @%epmd% -kill 202 | @goto :eof 203 | 204 | :: Start the Windows service 205 | :start 206 | @call :generate_config 207 | @%erlsrv% start %service_name% 208 | @goto :eof 209 | 210 | :: Stop the Windows service 211 | :stop 212 | @%erlsrv% stop %service_name% 213 | @goto :eof 214 | 215 | :: Relup and reldown 216 | :relup 217 | @if "" == "%2" ( 218 | echo Missing package argument 219 | echo Usage: %rel_name% %1 {package base name} 220 | echo NOTE {package base name} MUST NOT include the .tar.gz suffix 221 | set ERRORLEVEL=1 222 | exit /b %ERRORLEVEL% 223 | ) 224 | @%escript% "%rootdir%/bin/install_upgrade.escript" "install" "%rel_name%" "%node_type%" "%node_name%" "%cookie%" "%2" 225 | @goto :eof 226 | 227 | :: Start a console 228 | :console 229 | @call :generate_config 230 | @start "%rel_name% console" %werl% -config "%sys_config%" ^ 231 | -boot "%boot_script%" -boot_var ERTS_LIB_DIR "%erts_dir%"/lib ^ 232 | -env ERL_LIBS "%release_root_dir%"/lib ^ 233 | -pa "%release_root_dir%"/lib "%release_root_dir%"/lib/consolidated ^ 234 | -args_file "%vm_args%" ^ 235 | -user Elixir.IEx.CLI -extra --no-halt +iex 236 | 237 | @goto :eof 238 | 239 | :: Ping the running node 240 | :ping 241 | @%escript% %nodetool% ping %node_type% "%node_name%" -setcookie "%cookie%" 242 | @goto :eof 243 | 244 | :: List installed Erlang services 245 | :list 246 | @%erlsrv% list %service_name% 247 | @goto :eof 248 | 249 | :: Attach to a running node 250 | :attach 251 | @%escript% %nodetool% attach %werl% -boot "%clean_boot_script%" -config "%sys_config%" ^ 252 | -pa "%release_root_dir%"/lib "%release_root_dir%"/lib/consolidated ^ 253 | -hidden -noshell ^ 254 | -boot_var ERTS_LIB_DIR "%erts_dir%"/lib ^ 255 | -user Elixir.IEx.CLI "%node_type%" "%node_name%" ^ 256 | -setcookie "%cookie%" -args_file "%vm_args%" ^ 257 | -extra --no-halt +iex -"%node_type%" "%node_name%" --cookie "%cookie%" --remsh "%node_name%" 258 | @goto :eof 259 | -------------------------------------------------------------------------------- /priv/rel/files/boot_shim: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | SCRIPT_DIR="$(cd $(dirname "$0") && pwd -P)" 4 | RELEASE_ROOT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" 5 | RELEASES_DIR="$RELEASE_ROOT_DIR/releases" 6 | REL_NAME="{{{PROJECT_NAME}}}" 7 | REL_VSN=$(cat "$RELEASES_DIR"/start_erl.data | cut -d' ' -f2) 8 | ERTS_VSN=$(cat "$RELEASES_DIR"/start_erl.data | cut -d' ' -f1) 9 | 10 | exec "$RELEASES_DIR/$REL_VSN/$REL_NAME.sh" "$@" 11 | -------------------------------------------------------------------------------- /priv/rel/files/boot_shim.bat: -------------------------------------------------------------------------------- 1 | @if defined ELIXIR_CLI_ECHO (@echo on) else (@echo off) 2 | 3 | :: Determine if the current user has admin permissions or not. 4 | @echo Administrative permissions required. Detecting permissions... 5 | 6 | @net session >nul 2>&1 7 | if %errorLevel% == 0 (set is_admin="true") else (set is_admin="false") 8 | 9 | :: Set variables that describe the release 10 | @set rel_name={{{PROJECT_NAME}}} 11 | @set erl_opts={{{ERL_OPTS}}} 12 | 13 | :: Discover the release root directory from the directory of this script 14 | @set script_dir=%~dp0 15 | @for %%A in ("%script_dir%\..") do @( 16 | set release_root_dir=%%~sfA 17 | ) 18 | @set start_erl=%release_root_dir%\releases\start_erl.data 19 | @for /f "delims=" %%i in ('type %start_erl%') do @( 20 | set start_erl_data=%%i 21 | ) 22 | @for /f "tokens=1,* delims=\ " %%a in ("%start_erl_data%") do @( 23 | set erts_vsn=%%a 24 | set rel_vsn=%%b 25 | ) 26 | @set rel_dir=%release_root_dir%\releases\%rel_vsn% 27 | 28 | if %is_admin% == "true" ( 29 | call "%rel_dir%\%rel_name%.bat" %* 30 | ) else ( 31 | @echo You do not have administrator permissions. Please login as an administrator to proceed. 32 | ) 33 | 34 | -------------------------------------------------------------------------------- /priv/rel/files/install_upgrade.escript: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env escript 2 | %%! -noshell -noinput 3 | %% -*- mode: erlang;erlang-indent-level: 4;indent-tabs-mode: nil -*- 4 | %% ex: ft=erlang ts=4 sw=4 et 5 | 6 | -define(TIMEOUT, 300000). 7 | -define(INFO(Fmt,Args), io:format(Fmt,Args)). 8 | 9 | %% Unpack or upgrade to a new tar.gz release 10 | main(["unpack", RelName, NameType, NodeName, Cookie, VersionArg]) -> 11 | TargetNode = start_distribution(NameType, NodeName, Cookie), 12 | WhichReleases = which_releases(TargetNode), 13 | Version = parse_version(VersionArg), 14 | case proplists:get_value(Version, WhichReleases) of 15 | undefined -> 16 | %% not installed, so unpack tarball: 17 | ?INFO("Release ~s not found, attempting to unpack releases/~s/~s.tar.gz~n",[Version,Version,RelName]), 18 | ReleasePackage = Version ++ "/" ++ RelName, 19 | case rpc:call(TargetNode, release_handler, unpack_release, 20 | [ReleasePackage], ?TIMEOUT) of 21 | {ok, Vsn} -> 22 | ?INFO("Unpacked successfully: ~p~n", [Vsn]); 23 | {error, UnpackReason} -> 24 | print_existing_versions(TargetNode), 25 | ?INFO("Unpack failed: ~p~n",[UnpackReason]), 26 | erlang:halt(2) 27 | end; 28 | old -> 29 | %% no need to unpack, has been installed previously 30 | ?INFO("Release ~s is marked old, switching to it.~n",[Version]); 31 | unpacked -> 32 | ?INFO("Release ~s is already unpacked, now installing.~n",[Version]); 33 | current -> 34 | ?INFO("Release ~s is already installed and current. Making permanent.~n",[Version]); 35 | permanent -> 36 | ?INFO("Release ~s is already installed, and set permanent.~n",[Version]) 37 | end; 38 | main(["install", RelName, NameType, NodeName, Cookie, VersionArg]) -> 39 | TargetNode = start_distribution(NameType, NodeName, Cookie), 40 | WhichReleases = which_releases(TargetNode), 41 | Version = parse_version(VersionArg), 42 | case proplists:get_value(Version, WhichReleases) of 43 | undefined -> 44 | %% not installed, so unpack tarball: 45 | ?INFO("Release ~s not found, attempting to unpack releases/~s/~s.tar.gz~n",[Version,Version,RelName]), 46 | ReleasePackage = Version ++ "/" ++ RelName, 47 | case rpc:call(TargetNode, release_handler, unpack_release, 48 | [ReleasePackage], ?TIMEOUT) of 49 | {ok, Vsn} -> 50 | ?INFO("Unpacked successfully: ~p~n", [Vsn]), 51 | install_and_permafy(TargetNode, RelName, Vsn); 52 | {error, UnpackReason} -> 53 | print_existing_versions(TargetNode), 54 | ?INFO("Unpack failed: ~p~n",[UnpackReason]), 55 | erlang:halt(2) 56 | end; 57 | old -> 58 | %% no need to unpack, has been installed previously 59 | ?INFO("Release ~s is marked old, switching to it.~n",[Version]), 60 | install_and_permafy(TargetNode, RelName, Version); 61 | unpacked -> 62 | ?INFO("Release ~s is already unpacked, now installing.~n",[Version]), 63 | install_and_permafy(TargetNode, RelName, Version); 64 | current -> %% installed and in-use, just needs to be permanent 65 | ?INFO("Release ~s is already installed and current. Making permanent.~n",[Version]), 66 | permafy(TargetNode, RelName, Version); 67 | permanent -> 68 | ?INFO("Release ~s is already installed, and set permanent.~n",[Version]) 69 | end; 70 | main(_) -> 71 | erlang:halt(1). 72 | 73 | parse_version(V) when is_list(V) -> 74 | hd(string:tokens(V,"/")). 75 | 76 | install_and_permafy(TargetNode, RelName, Vsn) -> 77 | case rpc:call(TargetNode, release_handler, check_install_release, [Vsn], ?TIMEOUT) of 78 | {ok, _OtherVsn, _Desc} -> 79 | ok; 80 | {error, Reason} -> 81 | ?INFO("ERROR: release_handler:check_install_release failed: ~p~n",[Reason]), 82 | erlang:halt(3) 83 | end, 84 | case rpc:call(TargetNode, release_handler, install_release, [Vsn], ?TIMEOUT) of 85 | {ok, _, _} -> 86 | ?INFO("Installed Release: ~s~n", [Vsn]), 87 | permafy(TargetNode, RelName, Vsn), 88 | ok; 89 | {error, {no_such_release, Vsn}} -> 90 | VerList = 91 | iolist_to_binary( 92 | [io_lib:format("* ~s\t~s~n",[V,S]) || {V,S} <- which_releases(TargetNode)]), 93 | ?INFO("Installed versions:~n~s", [VerList]), 94 | ?INFO("ERROR: Unable to revert to '~s' - not installed.~n", [Vsn]), 95 | erlang:halt(2) 96 | end. 97 | 98 | permafy(TargetNode, RelName, Vsn) -> 99 | ok = rpc:call(TargetNode, release_handler, make_permanent, [Vsn], ?TIMEOUT), 100 | file:copy(filename:join(["bin", RelName++"-"++Vsn]), 101 | filename:join(["bin", RelName])), 102 | ?INFO("Made release permanent: ~p~n", [Vsn]), 103 | ok. 104 | 105 | which_releases(TargetNode) -> 106 | R = rpc:call(TargetNode, release_handler, which_releases, [], ?TIMEOUT), 107 | [ {V, S} || {_,V,_, S} <- R ]. 108 | 109 | print_existing_versions(TargetNode) -> 110 | VerList = iolist_to_binary([ 111 | io_lib:format("* ~s\t~s~n",[V,S]) 112 | || {V,S} <- which_releases(TargetNode) ]), 113 | ?INFO("Installed versions:~n~s", [VerList]). 114 | 115 | start_distribution(NameType, NodeName, Cookie) -> 116 | MyNode = make_script_node(NodeName), 117 | {ok, _Pid} = case NameType of 118 | "-sname" -> net_kernel:start([MyNode, shortnames]); 119 | "-name" -> net_kernel:start([MyNode, longnames]) 120 | end, 121 | erlang:set_cookie(node(), list_to_atom(Cookie)), 122 | TargetNode = list_to_atom(NodeName), 123 | case {net_kernel:connect_node(TargetNode), 124 | net_adm:ping(TargetNode)} of 125 | {true, pong} -> 126 | ok; 127 | {_, pang} -> 128 | io:format("Node ~p not responding to pings.\n", [TargetNode]), 129 | erlang:halt(1) 130 | end, 131 | {ok, Cwd} = file:get_cwd(), 132 | ok = rpc:call(TargetNode, file, set_cwd, [Cwd], ?TIMEOUT), 133 | TargetNode. 134 | 135 | make_script_node(Node) -> 136 | [Name, Host] = string:tokens(Node, "@"), 137 | list_to_atom(lists:concat([Name, "_upgrader_", os:getpid(), "@", Host])). 138 | -------------------------------------------------------------------------------- /priv/rel/files/nodetool: -------------------------------------------------------------------------------- 1 | %% -*- mode: erlang;erlang-indent-level: 4;indent-tabs-mode: nil -*- 2 | %% ex: ft=erlang ts=4 sw=4 et 3 | %% ------------------------------------------------------------------- 4 | %% 5 | %% nodetool: Helper Script for interacting with live nodes 6 | %% 7 | %% ------------------------------------------------------------------- 8 | 9 | main(Args) -> 10 | ok = start_epmd(), 11 | %% Extract the args 12 | {RestArgs, TargetNode} = process_args(Args, [], undefined), 13 | 14 | %% See if the node is currently running -- if it's not, we'll bail 15 | case {net_kernel:hidden_connect_node(TargetNode), net_adm:ping(TargetNode)} of 16 | {true, pong} -> 17 | ok; 18 | {_, pang} -> 19 | io:format("Node ~p not responding to pings.\n", [TargetNode]), 20 | halt(1) 21 | end, 22 | 23 | case RestArgs of 24 | ["ping"] -> 25 | %% If we got this far, the node already responsed to a ping, so just dump 26 | %% a "pong" 27 | io:format("pong\n"); 28 | ["stop"] -> 29 | io:format("~p\n", [rpc:call(TargetNode, init, stop, [], 60000)]); 30 | ["restart"] -> 31 | io:format("~p\n", [rpc:call(TargetNode, init, restart, [], 60000)]); 32 | ["reboot"] -> 33 | io:format("~p\n", [rpc:call(TargetNode, init, reboot, [], 60000)]); 34 | ["rpc", Module, Function | RpcArgs] -> 35 | case rpc:call(TargetNode, list_to_atom(Module), list_to_atom(Function), 36 | [RpcArgs], 60000) of 37 | ok -> 38 | ok; 39 | {badrpc, Reason} -> 40 | io:format("RPC to ~p failed: ~p\n", [TargetNode, Reason]), 41 | halt(1); 42 | _ -> 43 | halt(1) 44 | end; 45 | ["rpcterms", Module, Function] -> 46 | case rpc:call(TargetNode, list_to_atom(Module), list_to_atom(Function), [], 60000) of 47 | {badrpc, Reason} -> 48 | io:format("RPC to ~p failed: ~p\n", [TargetNode, Reason]), 49 | halt(1); 50 | Other -> 51 | io:format("~p\n", [Other]) 52 | end; 53 | ["rpcterms", Module, Function, ArgsAsString] -> 54 | case rpc:call(TargetNode, list_to_atom(Module), list_to_atom(Function), consult(ArgsAsString), 60000) of 55 | {badrpc, Reason} -> 56 | io:format("RPC to ~p failed: ~p\n", [TargetNode, Reason]), 57 | halt(1); 58 | Other -> 59 | io:format("~p\n", [Other]) 60 | end; 61 | Other -> 62 | io:format("Other: ~p\n", [Other]), 63 | io:format("Usage: nodetool {ping|stop|restart|reboot}\n") 64 | end, 65 | net_kernel:stop(). 66 | 67 | process_args([], Acc, TargetNode) -> 68 | {lists:reverse(Acc), TargetNode}; 69 | process_args(["-setcookie", Cookie | Rest], Acc, TargetNode) -> 70 | erlang:set_cookie(node(), list_to_atom(Cookie)), 71 | process_args(Rest, Acc, TargetNode); 72 | process_args(["-name", TargetName | Rest], Acc, _) -> 73 | ThisNode = append_node_suffix(TargetName, "_maint_"), 74 | {ok, _} = net_kernel:start([ThisNode, longnames]), 75 | process_args(Rest, Acc, nodename(TargetName)); 76 | process_args(["-sname", TargetName | Rest], Acc, _) -> 77 | ThisNode = append_node_suffix(TargetName, "_maint_"), 78 | {ok, _} = net_kernel:start([ThisNode, shortnames]), 79 | process_args(Rest, Acc, nodename(TargetName)); 80 | process_args([Arg | Rest], Acc, Opts) -> 81 | process_args(Rest, [Arg | Acc], Opts). 82 | 83 | 84 | start_epmd() -> 85 | [] = os:cmd("\"" ++ epmd_path() ++ "\" -daemon"), 86 | ok. 87 | 88 | epmd_path() -> 89 | ErtsBinDir = filename:dirname(escript:script_name()), 90 | Name = "epmd", 91 | case os:find_executable(Name, ErtsBinDir) of 92 | false -> 93 | case os:find_executable(Name) of 94 | false -> 95 | io:format("Could not find epmd.~n"), 96 | halt(1); 97 | GlobalEpmd -> 98 | GlobalEpmd 99 | end; 100 | Epmd -> 101 | Epmd 102 | end. 103 | 104 | 105 | nodename(Name) -> 106 | case string:tokens(Name, "@") of 107 | [_Node, _Host] -> 108 | list_to_atom(Name); 109 | [Node] -> 110 | [_, Host] = string:tokens(atom_to_list(node()), "@"), 111 | list_to_atom(lists:concat([Node, "@", Host])) 112 | end. 113 | 114 | append_node_suffix(Name, Suffix) -> 115 | case string:tokens(Name, "@") of 116 | [Node, Host] -> 117 | list_to_atom(lists:concat([Node, Suffix, os:getpid(), "@", Host])); 118 | [Node] -> 119 | list_to_atom(lists:concat([Node, Suffix, os:getpid()])) 120 | end. 121 | 122 | %% 123 | %% Given a string or binary, parse it into a list of terms, ala file:consult/0 124 | %% 125 | consult(Str) when is_list(Str) -> 126 | [Result] = consult([], Str, []), 127 | case is_list(Result) of 128 | true -> Result; 129 | false -> [Result] 130 | end; 131 | consult(Bin) when is_binary(Bin)-> 132 | [Result] = consult([], binary_to_list(Bin), []), 133 | case is_list(Result) of 134 | true -> Result; 135 | false -> [Result] 136 | end. 137 | 138 | consult(Cont, Str, Acc) -> 139 | case erl_scan:tokens(Cont, Str, 0) of 140 | {done, Result, Remaining} -> 141 | case Result of 142 | {ok, Tokens, _} -> 143 | {ok, Term} = erl_parse:parse_term(Tokens), 144 | consult([], Remaining, [Term | Acc]); 145 | {eof, _Other} -> 146 | lists:reverse(Acc); 147 | {error, Info, _} -> 148 | {error, Info} 149 | end; 150 | {more, Cont1} -> 151 | consult(Cont1, eof, Acc) 152 | end. 153 | -------------------------------------------------------------------------------- /priv/rel/files/release_definition.txt: -------------------------------------------------------------------------------- 1 | {release, { {{{PROJECT_NAME}}}, "{{{PROJECT_VERSION}}}" }, [ 2 | { {{{PROJECT_NAME}}}, "{{{PROJECT_VERSION}}}" }, 3 | elixir, 4 | iex, % needed for iex remote console 5 | sasl % required for upgrades 6 | ]}. 7 | -------------------------------------------------------------------------------- /priv/rel/files/sys.config: -------------------------------------------------------------------------------- 1 | %% Thanks to Ulf Wiger at Ericcson for these comments: 2 | %% 3 | %% This file is identified via the erl command line option -config File. 4 | %% Note that File should have no extension, e.g. 5 | %% erl -config .../sys (if this file is called sys.config) 6 | %% 7 | %% In this file, you can redefine application environment variables. 8 | %% This way, you don't have to modify the .app files of e.g. OTP applications. 9 | [{sasl, [{errlog_type, error}]}]. -------------------------------------------------------------------------------- /priv/rel/files/vm.args: -------------------------------------------------------------------------------- 1 | ## Name of the node 2 | -name {{{PROJECT_NAME}}}@127.0.0.1 3 | 4 | ## Cookie for distributed erlang 5 | -setcookie {{{PROJECT_NAME}}} 6 | 7 | ## Heartbeat management; auto-restarts VM if it dies or becomes unresponsive 8 | ## (Disabled by default..use with caution!) 9 | ##-heart 10 | 11 | ## Enable kernel poll and a few async threads 12 | ##+K true 13 | ##+A 5 14 | 15 | ## Increase number of concurrent ports/sockets 16 | ##-env ERL_MAX_PORTS 4096 17 | 18 | ## Tweak GC to run more often 19 | ##-env ERL_FULLSWEEP_AFTER 10 20 | -------------------------------------------------------------------------------- /priv/rel/relx.config: -------------------------------------------------------------------------------- 1 | {{{LIB_DIRS}}} 2 | 3 | %% Any older releases are next: 4 | {{{RELEASES}}} 5 | 6 | %% The latest release definition for the current project version 7 | {release, { {{{PROJECT_NAME}}}, "{{{PROJECT_VERSION}}}" }, [ 8 | { {{{PROJECT_NAME}}}, "{{{PROJECT_VERSION}}}" }, 9 | elixir, 10 | iex, % needed for iex remote console 11 | sasl % required for upgrades 12 | ]}. 13 | 14 | %% ERTS is included by default, but let's be explicit 15 | {include_erts, true}. 16 | 17 | %% Do not ship Erlang library sources as part of the release 18 | {include_src, false}. 19 | 20 | %% We're providing our own start script (see below) 21 | {extended_start_script, true}. 22 | {generate_start_script, false}. 23 | 24 | %% This copies our custom start script to the release bin directory 25 | {overlay, [ 26 | {mkdir, "releases/{{{PROJECT_VERSION}}}"}, 27 | {copy, "./sys.config", "releases/{{{PROJECT_VERSION}}}/sys.config"}, 28 | {copy, "./boot_shim", "bin/{{{PROJECT_NAME}}}"}, 29 | {copy, "./boot_shim.bat", "bin/{{{PROJECT_NAME}}}.bat"}, 30 | {copy, "./boot", "releases/{{{PROJECT_VERSION}}}/{{{PROJECT_NAME}}}.sh"}, 31 | {copy, "./boot.bat", "releases/{{{PROJECT_VERSION}}}/{{{PROJECT_NAME}}}.bat"} 32 | ]}. 33 | -------------------------------------------------------------------------------- /test/appups_test.exs: -------------------------------------------------------------------------------- 1 | defmodule AppupsTest do 2 | use ExUnit.Case, async: true 3 | 4 | import PathHelpers 5 | import ReleaseManager.Utils, only: [write_term: 2] 6 | 7 | @v1_path fixture_path("beams/v1") 8 | @v2_path fixture_path("beams/v2") 9 | @v1_app_path Path.join(@v1_path, "ebin/test.app") 10 | @v2_app_path Path.join(@v2_path, "ebin/test.app") 11 | 12 | setup do 13 | @v1_app_path |> write_term(v1_app()) 14 | @v2_app_path |> write_term(v2_app()) 15 | 16 | on_exit fn -> 17 | if @v1_app_path |> File.exists? do 18 | @v1_app_path |> File.rm! 19 | end 20 | if @v2_app_path |> File.exists? do 21 | @v2_app_path |> File.rm! 22 | end 23 | end 24 | 25 | :ok 26 | end 27 | 28 | test "generates valid .appup file" do 29 | {:ok, appup} = ReleaseManager.Appups.make(:test, "0.0.1", "0.0.2", @v1_path, @v2_path) 30 | assert appup == expected_appup() 31 | end 32 | 33 | defp v1_app do 34 | {:application,:test, 35 | [{:registered,[]}, 36 | {:description,'test'}, 37 | {:mod,{:"Elixir.Test",[]}}, 38 | {:applications,[:stdlib,:kernel,:elixir]}, 39 | {:vsn,'0.0.1'}, 40 | {:modules,[:"Elixir.Test",:"Elixir.Test.Server", 41 | :"Elixir.Test.Supervisor"]}]} 42 | end 43 | 44 | defp v2_app do 45 | {:application,:test, 46 | [{:registered,[]}, 47 | {:description,'test'}, 48 | {:mod,{:"Elixir.Test",[]}}, 49 | {:applications,[:exirc]}, 50 | {:vsn,'0.0.2'}, 51 | {:modules,[:"Elixir.Test",:"Elixir.Test.Server", 52 | :"Elixir.Test.Supervisor"]}]} 53 | end 54 | 55 | defp expected_appup do 56 | {'0.0.2', 57 | [{'0.0.1', 58 | [{:add_module, Asd}, 59 | {:update, Test.Server, {:advanced, []}}, 60 | {:load_module, Test}]}], 61 | [{'0.0.1', 62 | [{:delete_module, Asd}, 63 | {:update, Test.Server, {:advanced, []}}, 64 | {:load_module, Test}]}]} 65 | 66 | end 67 | end 68 | -------------------------------------------------------------------------------- /test/fixtures/beams/v1/ebin/Elixir.Test.Server.beam: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bitwalker/exrm/21bd28da70f818d895aae76e40966e91580d4667/test/fixtures/beams/v1/ebin/Elixir.Test.Server.beam -------------------------------------------------------------------------------- /test/fixtures/beams/v1/ebin/Elixir.Test.Supervisor.beam: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bitwalker/exrm/21bd28da70f818d895aae76e40966e91580d4667/test/fixtures/beams/v1/ebin/Elixir.Test.Supervisor.beam -------------------------------------------------------------------------------- /test/fixtures/beams/v1/ebin/Elixir.Test.beam: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bitwalker/exrm/21bd28da70f818d895aae76e40966e91580d4667/test/fixtures/beams/v1/ebin/Elixir.Test.beam -------------------------------------------------------------------------------- /test/fixtures/beams/v2/ebin/Elixir.Asd.beam: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bitwalker/exrm/21bd28da70f818d895aae76e40966e91580d4667/test/fixtures/beams/v2/ebin/Elixir.Asd.beam -------------------------------------------------------------------------------- /test/fixtures/beams/v2/ebin/Elixir.Test.Server.beam: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bitwalker/exrm/21bd28da70f818d895aae76e40966e91580d4667/test/fixtures/beams/v2/ebin/Elixir.Test.Server.beam -------------------------------------------------------------------------------- /test/fixtures/beams/v2/ebin/Elixir.Test.Supervisor.beam: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bitwalker/exrm/21bd28da70f818d895aae76e40966e91580d4667/test/fixtures/beams/v2/ebin/Elixir.Test.Supervisor.beam -------------------------------------------------------------------------------- /test/fixtures/beams/v2/ebin/Elixir.Test.beam: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bitwalker/exrm/21bd28da70f818d895aae76e40966e91580d4667/test/fixtures/beams/v2/ebin/Elixir.Test.beam -------------------------------------------------------------------------------- /test/fixtures/beams/v2/ebin/test.appup: -------------------------------------------------------------------------------- 1 | {"0.0.2", 2 | [{"0.0.1", 3 | [{add_module,'Elixir.Asd'}, 4 | {update,'Elixir.Test.Server',{advanced,[]}}, 5 | {load_module,'Elixir.Test'}]}], 6 | [{"0.0.1", 7 | [{delete_module,'Elixir.Asd'}, 8 | {update,'Elixir.Test.Server',{advanced,[]}}, 9 | {load_module,'Elixir.Test'}]}]}. 10 | -------------------------------------------------------------------------------- /test/fixtures/configs/merged_relx.config: -------------------------------------------------------------------------------- 1 | {lib_dirs,["/Users/Schoens/.exenv/versions/0.14.3/lib","../../_build/prod"]}. 2 | 3 | %% The latest release definition for the current project version 4 | {release, { test, "0.0.1" }, [ 5 | { test, "0.0.1" }, 6 | elixir, 7 | iex, % needed for iex remote console 8 | sasl, % required for upgrades 9 | {neotoma, load} 10 | ]}. 11 | 12 | %% ERTS is included by default, but let's be explicit 13 | {include_erts, true}. 14 | 15 | %% Do not ship Erlang library sources as part of the release 16 | {include_src, true}. 17 | 18 | %% We're providing our own start script (see below) 19 | {extended_start_script, true}. 20 | {generate_start_script, false}. 21 | 22 | %% This copies our custom start script to the release bin directory 23 | {overlay, [ 24 | {mkdir, "releases/0.0.1"}, 25 | {copy, "./sys.config", "releases/0.0.1/sys.config"}, 26 | {copy, "./boot", "bin/test"}, 27 | {mkdir, "releases/stuff"}, 28 | {copy, "./things", "releases/things"} 29 | ]}. 30 | 31 | {system_libs, "./_erlang/lib"}. 32 | -------------------------------------------------------------------------------- /test/fixtures/configs/new_relx.config: -------------------------------------------------------------------------------- 1 | {release, { test, "0.0.1" }, [ 2 | {neotoma, load} 3 | ]}. 4 | 5 | {system_libs, "./_erlang/lib"}. 6 | 7 | {include_src,true}. 8 | 9 | {overlay, [ 10 | {mkdir, "releases/stuff"}, 11 | {copy, "./things", "releases/things"} 12 | ]}. 13 | -------------------------------------------------------------------------------- /test/fixtures/configs/old_relx.config: -------------------------------------------------------------------------------- 1 | {lib_dirs,["/Users/Schoens/.exenv/versions/0.14.3/lib","../../_build/prod"]}. 2 | 3 | 4 | 5 | %% Any older releases are next: 6 | 7 | 8 | %% The latest release definition for the current project version 9 | {release, { test, "0.0.1" }, [ 10 | { test, "0.0.1" }, 11 | elixir, 12 | iex, % needed for iex remote console 13 | sasl % required for upgrades 14 | ]}. 15 | 16 | %% ERTS is included by default, but let's be explicit 17 | {include_erts, true}. 18 | 19 | %% Do not ship Erlang library sources as part of the release 20 | {include_src, false}. 21 | 22 | %% We're providing our own start script (see below) 23 | {extended_start_script, true}. 24 | {generate_start_script, false}. 25 | 26 | %% This copies our custom start script to the release bin directory 27 | {overlay, [ 28 | {mkdir, "releases/0.0.1"}, 29 | {copy, "./sys.config", "releases/0.0.1/sys.config"}, 30 | {copy, "./boot", "bin/test"} 31 | ]}. -------------------------------------------------------------------------------- /test/fixtures/example_app/.gitignore: -------------------------------------------------------------------------------- 1 | /_build 2 | /_elixir 3 | /deps 4 | /rel 5 | relx 6 | erl_crash.dump 7 | *.ez 8 | mix.lock 9 | -------------------------------------------------------------------------------- /test/fixtures/example_app/config/config.all.exs: -------------------------------------------------------------------------------- 1 | use Mix.Config 2 | 3 | config :test, 4 | env: :dev 5 | -------------------------------------------------------------------------------- /test/fixtures/example_app/config/config.dev.exs: -------------------------------------------------------------------------------- 1 | use Mix.Config 2 | 3 | config :test, 4 | env: :dev 5 | -------------------------------------------------------------------------------- /test/fixtures/example_app/config/config.exs: -------------------------------------------------------------------------------- 1 | use Mix.Config 2 | 3 | config :test, 4 | foo: "nope", 5 | env: :wat, 6 | "debug_level": {:on, [:passive]} 7 | 8 | import_config "config.#{Mix.env}.exs" 9 | -------------------------------------------------------------------------------- /test/fixtures/example_app/config/config.prod.exs: -------------------------------------------------------------------------------- 1 | use Mix.Config 2 | 3 | config :test, 4 | env: :prod 5 | -------------------------------------------------------------------------------- /test/fixtures/example_app/config/config.test.exs: -------------------------------------------------------------------------------- 1 | use Mix.Config 2 | 3 | config :test, 4 | env: :test 5 | -------------------------------------------------------------------------------- /test/fixtures/example_app/config/explicit_config.exs: -------------------------------------------------------------------------------- 1 | use Mix.Config 2 | 3 | config :test, foo: :explicit 4 | -------------------------------------------------------------------------------- /test/fixtures/example_app/config/test.conf: -------------------------------------------------------------------------------- 1 | # Documentation for test.foo goes here. 2 | test.foo = bar 3 | 4 | # The current execution environment 5 | test.env = dev 6 | 7 | # Set the appropriate tracing options. 8 | # Valid options are: 9 | # - active: tracing is enabled 10 | # - active-debug: tracing is enabled, with debugging 11 | # - passive: tracing must be manually invoked 12 | # - off: tracing is disabled 13 | # Defaults to off 14 | test.debug.level = off 15 | 16 | test.some_val = 100 17 | -------------------------------------------------------------------------------- /test/fixtures/example_app/config/test.schema.exs: -------------------------------------------------------------------------------- 1 | [ 2 | import: [ 3 | :fake_project 4 | ], 5 | mappings: [ 6 | "test.foo": [ 7 | doc: "Documentation for test.foo goes here.", 8 | to: "test.foo", 9 | datatype: :binary, 10 | default: "bar" 11 | ], 12 | "test.env": [ 13 | doc: "The current execution environment", 14 | to: "test.env", 15 | datatype: :atom, 16 | default: :dev 17 | ], 18 | "test.debug.level": [ 19 | doc: """ 20 | Set the appropriate tracing options. 21 | Valid options are: 22 | - active: tracing is enabled 23 | - active-debug: tracing is enabled, with debugging 24 | - passive: tracing must be manually invoked 25 | - off: tracing is disabled 26 | 27 | Defaults to off 28 | """, 29 | to: "test.debug_level", 30 | datatype: [ 31 | enum: [ 32 | :active, 33 | :"active-debug", 34 | :passive, 35 | :off 36 | ] 37 | ], 38 | default: :off 39 | ], 40 | "test.some_val": [ 41 | doc: """ 42 | Just a some val 43 | """, 44 | to: "test.some_val", 45 | datatype: :integer, 46 | default: 10 47 | ] 48 | ], 49 | transforms: [ 50 | "test.debug_level": fn conf -> 51 | case Conform.Conf.get(conf, "test.debug_level") do 52 | [{_, :active}] -> 53 | {:on, []} 54 | [{_, :"active-debug"}] -> 55 | {:on, [:debug]} 56 | [{_, :passive}] -> 57 | {:on, [:passive]} 58 | [{_, :off}] -> 59 | {:off, []} 60 | [] -> 61 | end 62 | end, 63 | "test.some_val": fn conf -> 64 | case Conform.Conf.get(conf, "test.some_val") do 65 | [{_, x}] when is_integer(x) -> FakeProject.inc_some_val(x) 66 | [{_, x}] -> x 67 | end 68 | end 69 | ] 70 | ] 71 | -------------------------------------------------------------------------------- /test/fixtures/example_app/lib/test.ex: -------------------------------------------------------------------------------- 1 | defmodule Test do 2 | use Application 3 | 4 | # See http://elixir-lang.org/docs/stable/Application.Behaviour.html 5 | # for more information on OTP Applications 6 | def start(_type, _args) do 7 | Test.Supervisor.start_link 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /test/fixtures/example_app/lib/test/server.ex: -------------------------------------------------------------------------------- 1 | defmodule Test.Server do 2 | use GenServer 3 | 4 | def start_link() do 5 | GenServer.start_link(__MODULE__, [], [name: __MODULE__]) 6 | end 7 | 8 | def init([]) do 9 | { :ok, [] } 10 | end 11 | 12 | def handle_call(:ping, _from, state) do 13 | { :reply, :v1, state} 14 | end 15 | 16 | end 17 | -------------------------------------------------------------------------------- /test/fixtures/example_app/lib/test/supervisor.ex: -------------------------------------------------------------------------------- 1 | defmodule Test.Supervisor do 2 | 3 | def start_link do 4 | import Supervisor.Spec, warn: false 5 | 6 | children = [ 7 | # Define workers and child supervisors to be supervised 8 | worker(Test.Server, []) 9 | ] 10 | 11 | Supervisor.start_link(children, [strategy: :one_for_one, name: Test.Supervisor]) 12 | end 13 | 14 | end 15 | -------------------------------------------------------------------------------- /test/fixtures/example_app/mix.exs: -------------------------------------------------------------------------------- 1 | defmodule Test.Mixfile do 2 | use Mix.Project 3 | 4 | def project do 5 | [ app: :test, 6 | version: "0.0.1", 7 | deps: deps ] 8 | end 9 | 10 | # Configuration for the OTP application 11 | def application do 12 | [mod: { Test, [] }, 13 | applications: [:fake_project]] 14 | end 15 | 16 | # Returns the list of dependencies in the format: 17 | # { :foobar, git: "https://github.com/elixir-lang/foobar.git", tag: "0.1" } 18 | # 19 | # To specify particular versions, regardless of the tag, do: 20 | # { :barbat, "~> 0.1", github: "elixir-lang/barbat" } 21 | defp deps do 22 | [{:exrm, path: "../../../", override: true}, 23 | {:conform_exrm, github: "bitwalker/conform_exrm", override: true}, 24 | {:conform, github: "bitwalker/conform", override: true}, 25 | {:fake_project, path: "../fake_project"}] 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /test/fixtures/example_app/priv/sample.txt: -------------------------------------------------------------------------------- 1 | The Chiss were governed by an oligarchy of extended Ruling Families from House Palace, located in the city of Csaplar. Each clan with the families was headed by leaders known as Aristocra who wore particular colors to indicate their clan and family loyalties. Standard and day-to-day decisions were made by a democratically elected parliamentary body from each of the 28 colonies. Issues were siphoned up through the parliament to a cabinet of appointed governors, and then to the ruling families, where a decision made by the parliament and/or cabinet could be approved for action. Each of the extended ruling families was responsible for a set of government affairs to manage: House Sabosen was responsible for social issues such as justice, health, and education; House Inrokini was responsible for industry and science; House Csapla was responsible for colonial affairs, agriculture and redistribution of resources; and House Nuruodo was responsible for military and foreign affairs. The Csapla's redistribution of resources amongst the Chiss colonies and Csilla was particularly important, given the collectivist-socialist economic system of the Chiss, and led to their relative position at the head of the families, with the others acting in an advisory role. House Nuruodo would be ranked as second, considering the importance of warfare on Chiss society. 2 | 3 | Each family was equally represented in the Chiss government, although they went to great lengths to ensure that family identity was eliminated wherever possible. The leaders of the Chiss did not use names, but instead wore colorful robes to distinguish themselves. This helped ensure that decisions were reached in a fair and equitable method. Although the four extended ruling families were not known to face power struggles, five lesser clans often vied for greater authority and power, such as House Chaf, which was considered the fifth ruling family. 4 | 5 | The vast financial holdings of the Chiss kept their supply lines running and also provided them access to a number of independent shipyards which they used to keep their vessels on constant patrol. It was through such methods that the Chiss maintained a poacher base at the Etyyy hunting grounds on Kashyyyk. 6 | -------------------------------------------------------------------------------- /test/fixtures/example_app/relx.config: -------------------------------------------------------------------------------- 1 | {include_src,true}. 2 | -------------------------------------------------------------------------------- /test/fixtures/fake_project/.gitignore: -------------------------------------------------------------------------------- 1 | /_build 2 | /cover 3 | /deps 4 | erl_crash.dump 5 | *.ez 6 | -------------------------------------------------------------------------------- /test/fixtures/fake_project/lib/fake_project.ex: -------------------------------------------------------------------------------- 1 | defmodule FakeProject do 2 | def inc_some_val(val), do: val + 1 3 | end 4 | -------------------------------------------------------------------------------- /test/fixtures/fake_project/mix.exs: -------------------------------------------------------------------------------- 1 | defmodule FakeProject.Mixfile do 2 | use Mix.Project 3 | 4 | def project do 5 | [app: :fake_project, 6 | version: "0.0.1", 7 | deps: deps] 8 | end 9 | 10 | # Configuration for the OTP application 11 | # 12 | # Type `mix help compile.app` for more information 13 | def application do 14 | [applications: [:logger]] 15 | end 16 | 17 | # Dependencies can be Hex packages: 18 | # 19 | # {:mydep, "~> 0.3.0"} 20 | # 21 | # Or git/path repositories: 22 | # 23 | # {:mydep, git: "https://github.com/elixir-lang/mydep.git", tag: "0.1.0"} 24 | # 25 | # Type `mix help deps` for more examples and options 26 | defp deps do 27 | [] 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /test/plugin_test.exs: -------------------------------------------------------------------------------- 1 | defmodule PluginTest do 2 | use ExUnit.Case 3 | alias ReleaseManager.Utils 4 | 5 | test "can fetch a list of plugins" do 6 | active = [ 7 | ReleaseManager.Plugin.Appups, 8 | ReleaseManager.Plugin.Consolidation, 9 | ] |> Enum.sort 10 | 11 | assert active == ReleaseManager.Plugin.load_all |> Enum.sort 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | 3 | defmodule PathHelpers do 4 | def fixture_path() do 5 | Path.expand("fixtures", __DIR__) 6 | end 7 | 8 | def fixture_path(extra) do 9 | Path.join(fixture_path(), extra) 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /test/utils_test.exs: -------------------------------------------------------------------------------- 1 | defmodule UtilsTest do 2 | use ExUnit.Case, async: true 3 | use Bitwise, only_operators: true 4 | 5 | import ExUnit.CaptureIO 6 | 7 | import PathHelpers 8 | 9 | alias ReleaseManager.Utils 10 | 11 | @example_app_path fixture_path("example_app") 12 | @old_path fixture_path("configs/old_relx.config") 13 | @new_path fixture_path("configs/new_relx.config") 14 | @expected_path fixture_path("configs/merged_relx.config") 15 | 16 | defmacrop with_app(body) do 17 | quote do 18 | cwd = File.cwd! 19 | File.cd! @example_app_path 20 | unquote(body) 21 | File.cd! cwd 22 | end 23 | end 24 | 25 | test "can merge two relx.config files" do 26 | old = @old_path |> Utils.read_terms 27 | new = @new_path |> Utils.read_terms 28 | expected = @expected_path |> Utils.read_terms 29 | 30 | merged = Utils.merge(old, new) 31 | 32 | assert expected == merged 33 | end 34 | 35 | test "can read terms from string" do 36 | config = @expected_path |> File.read! 37 | expected = @expected_path |> Utils.read_terms 38 | terms = Utils.string_to_terms(config) 39 | 40 | assert expected == terms 41 | end 42 | 43 | test "can run a function in a specific Mix environment" do 44 | execution_env = Utils.with_env :prod, fn -> Mix.env end 45 | assert :prod = execution_env 46 | end 47 | 48 | test "can load the current project configuration for a given environment" do 49 | with_app do 50 | [test: config] = Utils.load_config(:prod) 51 | assert List.keymember?(config, :foo, 0) 52 | end 53 | end 54 | 55 | test "can load the current project configuration from project's specified location" do 56 | project_config = [config_path: "config/explicit_config.exs"] 57 | with_app do 58 | [test: config] = Utils.load_config(:prod, project_config) 59 | assert Keyword.fetch!(config, :foo) == :explicit 60 | end 61 | end 62 | 63 | test "can invoke mix to perform a task for a given environment" do 64 | with_app do 65 | assert :ok = Utils.mix("clean", :prod) 66 | end 67 | end 68 | 69 | test "can get the current elixir library path" do 70 | elixir_path = Utils.get_elixir_lib_paths |> Enum.filter(&(String.ends_with?("/elixir/ebin", &1))) 71 | path = Path.join(elixir_path, "../bin/elixir") 72 | {result, _} = System.cmd(path, ["--version"]) 73 | version = result |> String.strip(?\n) 74 | assert String.contains?(version, "Elixir #{System.version}") 75 | end 76 | 77 | @tag :expensive 78 | @tag timeout: 120000 # 120s 79 | test "can build a release and boot it up" do 80 | with_app do 81 | #capture_io(fn -> 82 | # Build release 83 | assert :ok = Utils.mix("do deps.get, compile", Mix.env, :quiet) 84 | assert :ok = Utils.mix("release --no-confirm-missing --verbosity=verbose", Mix.env, :verbose) 85 | assert [{"test", "0.0.1"}] == Utils.get_releases("test") 86 | # Boot it, ping it, and shut it down 87 | bin_path = Path.join([File.cwd!, "rel", "test", "bin", "test"]) 88 | assert {_, 0} = System.cmd(bin_path, ["start"]) 89 | :timer.sleep(1000) # Required, since starting up takes a sec 90 | assert {result, 0} = System.cmd(bin_path, ["ping"]) 91 | assert String.contains?(result, "pong") 92 | assert {result, 0} = System.cmd(bin_path, ["stop"]) 93 | assert String.contains?(result, "ok") 94 | sys_config_path = Path.join([File.cwd!, "rel", "test", "running-config", "sys.config"]) 95 | {res, sysconfig_content} = :file.consult(to_char_list(sys_config_path)) 96 | assert :ok = res 97 | some_val = Keyword.get(List.first(sysconfig_content), :test) |> Keyword.get(:some_val) 98 | assert 101 = some_val 99 | sys_config_rel_path = Path.join([File.cwd!, "rel", "test", "releases", "0.0.1", "sys.config"]) 100 | {:ok, info } = File.stat(sys_config_rel_path) 101 | assert (info.mode &&& 0o0777) == 0o600 102 | #end) 103 | end 104 | end 105 | 106 | test "can compare semver versions" do 107 | assert ["1.0.10"|_] = Utils.sort_versions(["1.0.1", "1.0.2", "1.0.9", "1.0.10"]) 108 | end 109 | 110 | test "can compare non-semver versions" do 111 | assert ["1.3", "1.2", "1.1"] = Utils.sort_versions(["1.1", "1.3", "1.2"]) 112 | end 113 | 114 | test "can compare complex versions" do 115 | expected = [ 116 | "0.0.3-142-deadbeef", 117 | "0.0.3-43-aaaabbbb", 118 | "0.0.3-5-ccccdddd", 119 | "0.0.3", 120 | "0.0.2", 121 | "0.0.1-2-a1d2g3f", 122 | "0.0.1-1-deadbeef", 123 | "0.0.1" 124 | ] 125 | result = Utils.sort_versions([ 126 | "0.0.3", 127 | "0.0.2", 128 | "0.0.3-43-aaaabbbb", 129 | "0.0.1-1-deadbeef", 130 | "0.0.1-2-a1d2g3f", 131 | "0.0.3-142-deadbeef", 132 | "0.0.3-5-ccccdddd", 133 | "0.0.1" 134 | ]) 135 | assert expected == result 136 | end 137 | 138 | end 139 | --------------------------------------------------------------------------------