diff options
-rw-r--r-- | .gitignore | 2 | ||||
-rw-r--r-- | .travis.yml | 13 | ||||
-rw-r--r-- | Makefile | 10 | ||||
-rw-r--r-- | docs/conf.py | 285 | ||||
-rw-r--r-- | docs/configuration.rst | 89 | ||||
-rw-r--r-- | docs/docs.mkf | 154 | ||||
-rw-r--r-- | docs/index.rst | 22 | ||||
-rw-r--r-- | docs/overview.rst | 58 | ||||
-rw-r--r-- | examples/relcool.config | 74 | ||||
-rw-r--r-- | examples/relcool_simple.config | 19 | ||||
-rw-r--r-- | rebar.config | 19 | ||||
-rw-r--r-- | src/rcl_app_info.erl | 26 | ||||
-rw-r--r-- | src/rcl_cmd_args.erl | 2 | ||||
-rw-r--r-- | src/rcl_provider.erl | 7 | ||||
-rw-r--r-- | src/rcl_prv_assembler.erl | 37 | ||||
-rw-r--r-- | src/rcl_prv_config.erl | 3 | ||||
-rw-r--r-- | src/rcl_prv_discover.erl | 53 | ||||
-rw-r--r-- | src/rcl_state.erl | 14 | ||||
-rw-r--r-- | src/relcool.erl | 34 | ||||
-rw-r--r-- | test/rclt_release_SUITE.erl | 58 |
20 files changed, 311 insertions, 668 deletions
@@ -8,5 +8,5 @@ relcool src/rcl_goal.erl logs test/*_data -relcool_output/* +_rel/* .*
\ No newline at end of file diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..3832235 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,13 @@ +language: erlang +otp_release: + - R15B02 + - R15B01 + - R15B + - R14B04 + - R14B03 + - R14B02 +script: "make rebuild" +branches: + only: + - master + - next
\ No newline at end of file @@ -41,15 +41,6 @@ endif all: compile escript dialyzer test # ============================================================================= -# Include relevant sub-makefiles. -# ============================================================================= -# These are not subdirs, they are just additional makefile information -# that happens to live subdirectories - -include $(CURDIR)/docs/docs.mkf - - -# ============================================================================= # Rules to build the system # ============================================================================= @@ -107,6 +98,7 @@ clean-common-test-data: clean: clean-common-test-data - rm -rf $(CURDIR)/test/*.beam - rm -rf $(CURDIR)/logs + - rm -rf $(CURDIR)/ebin $(REBAR) skip_deps=true clean distclean: clean diff --git a/docs/conf.py b/docs/conf.py deleted file mode 100644 index f29af4c..0000000 --- a/docs/conf.py +++ /dev/null @@ -1,285 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Relcool documentation build configuration file, created by -# sphinx-quickstart on Mon Sep 10 10:04:06 2012. -# -# This file is execfile()d with the current directory set to its containing dir. -# -# Note that not all possible configuration values are present in this -# autogenerated file. -# -# All configuration values have a default; values that are commented out -# serve to show the default. - -import sys, os - -# If extensions (or modules to document with autodoc) are in another directory, -# add these directories to sys.path here. If the directory is relative to the -# documentation root, use os.path.abspath to make it absolute, like shown here. -#sys.path.insert(0, os.path.abspath('.')) - -# -- General configuration ----------------------------------------------------- - -# If your documentation needs a minimal Sphinx version, state it here. -#needs_sphinx = '1.0' - -# Add any Sphinx extension module names here, as strings. They can be extensions -# coming with Sphinx (named 'sphinx.ext.*') or your custom ones. -extensions = [] - -# Add any paths that contain templates here, relative to this directory. -templates_path = ['_templates'] - -# The suffix of source filenames. -source_suffix = '.rst' - -# The encoding of source files. -#source_encoding = 'utf-8-sig' - -# The master toctree document. -master_doc = 'index' - -# General information about the project. -project = u'Relcool' -copyright = u'2012, Eric B MErritt' - -# The version info for the project you're documenting, acts as replacement for -# |version| and |release|, also used in various other places throughout the -# built documents. -# -# The short X.Y version. -version = '0.0.1' -# The full version, including alpha/beta/rc tags. -release = '0.0.1' - -# The language for content autogenerated by Sphinx. Refer to documentation -# for a list of supported languages. -#language = None - -# There are two options for replacing |today|: either, you set today to some -# non-false value, then it is used: -#today = '' -# Else, today_fmt is used as the format for a strftime call. -#today_fmt = '%B %d, %Y' - -# List of patterns, relative to source directory, that match files and -# directories to ignore when looking for source files. -exclude_patterns = ['_build'] - -# The reST default role (used for this markup: `text`) to use for all documents. -#default_role = None - -# If true, '()' will be appended to :func: etc. cross-reference text. -#add_function_parentheses = True - -# If true, the current module name will be prepended to all description -# unit titles (such as .. function::). -#add_module_names = True - -# If true, sectionauthor and moduleauthor directives will be shown in the -# output. They are ignored by default. -#show_authors = False - -# The name of the Pygments (syntax highlighting) style to use. -pygments_style = 'sphinx' - -# A list of ignored prefixes for module index sorting. -#modindex_common_prefix = [] - - -# -- Options for HTML output --------------------------------------------------- - -# The theme to use for HTML and HTML Help pages. See the documentation for -# a list of builtin themes. -html_theme = 'default' - -# Theme options are theme-specific and customize the look and feel of a theme -# further. For a list of options available for each theme, see the -# documentation. -#html_theme_options = {} - -# Add any paths that contain custom themes here, relative to this directory. -#html_theme_path = [] - -# The name for this set of Sphinx documents. If None, it defaults to -# "<project> v<release> documentation". -#html_title = None - -# A shorter title for the navigation bar. Default is the same as html_title. -#html_short_title = None - -# The name of an image file (relative to this directory) to place at the top -# of the sidebar. -#html_logo = None - -# The name of an image file (within the static path) to use as favicon of the -# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 -# pixels large. -#html_favicon = None - -# Add any paths that contain custom static files (such as style sheets) here, -# relative to this directory. They are copied after the builtin static files, -# so a file named "default.css" will overwrite the builtin "default.css". -html_static_path = ['_static'] - -# If not '', a 'Last updated on:' timestamp is inserted at every page bottom, -# using the given strftime format. -#html_last_updated_fmt = '%b %d, %Y' - -# If true, SmartyPants will be used to convert quotes and dashes to -# typographically correct entities. -#html_use_smartypants = True - -# Custom sidebar templates, maps document names to template names. -#html_sidebars = {} - -# Additional templates that should be rendered to pages, maps page names to -# template names. -#html_additional_pages = {} - -# If false, no module index is generated. -#html_domain_indices = True - -# If false, no index is generated. -#html_use_index = True - -# If true, the index is split into individual pages for each letter. -#html_split_index = False - -# If true, links to the reST sources are added to the pages. -#html_show_sourcelink = True - -# If true, "Created using Sphinx" is shown in the HTML footer. Default is True. -#html_show_sphinx = True - -# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. -#html_show_copyright = True - -# If true, an OpenSearch description file will be output, and all pages will -# contain a <link> tag referring to it. The value of this option must be the -# base URL from which the finished HTML is served. -#html_use_opensearch = '' - -# This is the file name suffix for HTML files (e.g. ".xhtml"). -#html_file_suffix = None - -# Output file base name for HTML help builder. -htmlhelp_basename = 'Relcooldoc' - - -# -- Options for LaTeX output -------------------------------------------------- - -latex_elements = { -# The paper size ('letterpaper' or 'a4paper'). -#'papersize': 'letterpaper', - -# The font size ('10pt', '11pt' or '12pt'). -#'pointsize': '10pt', - -# Additional stuff for the LaTeX preamble. -#'preamble': '', -} - -# Grouping the document tree into LaTeX files. List of tuples -# (source start file, target name, title, author, documentclass [howto/manual]). -latex_documents = [ - ('index', 'Relcool.tex', u'Relcool Documentation', - u'Eric B MErritt', 'manual'), -] - -# The name of an image file (relative to this directory) to place at the top of -# the title page. -#latex_logo = None - -# For "manual" documents, if this is true, then toplevel headings are parts, -# not chapters. -#latex_use_parts = False - -# If true, show page references after internal links. -#latex_show_pagerefs = False - -# If true, show URL addresses after external links. -#latex_show_urls = False - -# Documents to append as an appendix to all manuals. -#latex_appendices = [] - -# If false, no module index is generated. -#latex_domain_indices = True - - -# -- Options for manual page output -------------------------------------------- - -# One entry per manual page. List of tuples -# (source start file, name, description, authors, manual section). -man_pages = [ - ('index', 'relcool', u'Relcool Documentation', - [u'Eric B MErritt'], 1) -] - -# If true, show URL addresses after external links. -#man_show_urls = False - - -# -- Options for Texinfo output ------------------------------------------------ - -# Grouping the document tree into Texinfo files. List of tuples -# (source start file, target name, title, author, -# dir menu entry, description, category) -texinfo_documents = [ - ('index', 'Relcool', u'Relcool Documentation', - u'Eric B MErritt', 'Relcool', 'One line description of project.', - 'Miscellaneous'), -] - -# Documents to append as an appendix to all manuals. -#texinfo_appendices = [] - -# If false, no module index is generated. -#texinfo_domain_indices = True - -# How to display URL addresses: 'footnote', 'no', or 'inline'. -#texinfo_show_urls = 'footnote' - - -# -- Options for Epub output --------------------------------------------------- - -# Bibliographic Dublin Core info. -epub_title = u'Relcool' -epub_author = u'Eric B MErritt' -epub_publisher = u'Eric B MErritt' -epub_copyright = u'2012, Eric B MErritt' - -# The language of the text. It defaults to the language option -# or en if the language is not set. -#epub_language = '' - -# The scheme of the identifier. Typical schemes are ISBN or URL. -#epub_scheme = '' - -# The unique identifier of the text. This can be a ISBN number -# or the project homepage. -#epub_identifier = '' - -# A unique identification for the text. -#epub_uid = '' - -# A tuple containing the cover image and cover page html template filenames. -#epub_cover = () - -# HTML files that should be inserted before the pages created by sphinx. -# The format is a list of tuples containing the path and title. -#epub_pre_files = [] - -# HTML files shat should be inserted after the pages created by sphinx. -# The format is a list of tuples containing the path and title. -#epub_post_files = [] - -# A list of files that should not be packed into the epub file. -#epub_exclude_files = [] - -# The depth of the table of contents in toc.ncx. -#epub_tocdepth = 3 - -# Allow duplicate toc entries. -#epub_tocdup = True diff --git a/docs/configuration.rst b/docs/configuration.rst deleted file mode 100644 index 4e84deb..0000000 --- a/docs/configuration.rst +++ /dev/null @@ -1,89 +0,0 @@ -Configuration -============= - -Configuration of reltool is done via Erlang key, value elements in the -`relcool.config` file. There are various configuration elements that -can appear, each affects the running of the system in specific -ways. Note that these must be complete Erlang terms terminated by a -`.`. - -Paths ------ - -Paths is a way to add additional code paths (that point to Erlang beam -files) to the system. These are not used for dependency resolution, -they are only used as code paths. The main use for this is simply to -provide a non-resolvable path for additional providers. - -the path confuration element looks as follows:: - - {paths, [<list of directory paths>]}. - -So if we wanted to add `/usr/local/lib` and `/opt/lib` to the code -paths for the system we could add the following:: - - {paths, ["/usr/local/lib", "/opt/lib"]}. - -to our configuration. - -Providers ---------- - -Providers are how the Relcool system can be extended by a user. They -are basically Erlang modules that implement the `rcl_provider` -behaviour and are available via a code path. See the section on -providers for more information. - -The `providers` element provides a completely new list of providers, -replacing any providers that may already exist. The provider element -is as follows:: - - {providers, <list of module names>}. - -Lets say I have three providers; `my_custom_assembler`, -`my_rpm_assembler` and `my_deb_assembler`. I could make these the -complete list of providers by doing the following in my config:: - - {providers, [my_custom_assembler, - my_rpm_assembler, - my_deb_assembler]}. - -Order is important in the providers as they will be executed in the -order specified. - -Add Providers -------------- - -`add_providers` is very similar to `providers` with the exception that -the listed providers are added to the end of the current list of -providers rather then replacing the list all together. Add providres -looks as follows:: - - - {add_providers, <list of module names>}. - -Lets take our previous example but only add `my_rpm_assembler` and -`my_deb_assembler` to the existing list of providers. We do this by -adding the following:: - - {add_providers, [my_rpm_assembler, - my_deb_assembler]}. - -Releases --------- - -Release configuration is the bread and butter, the blood and bones of -the Relcool system. It provides the framework for realizing -dependencies in the system. The release element basically minics the -standard _Erlang/OTP Release Metadata file format: -http://www.erlang.org/doc/man/rel.html. There are two main -differences. The first is that you don't have to specify a complete -list of applications and you may specify version constraints instead -of hard versions. - - -{release, {relname, vsn}, - app goals} -{release, {relname, vsn}, -{erts, vsn}, -app goals} diff --git a/docs/docs.mkf b/docs/docs.mkf deleted file mode 100644 index 6987063..0000000 --- a/docs/docs.mkf +++ /dev/null @@ -1,154 +0,0 @@ -# -*- mode: Makefile; fill-column: 80; comment-column: 75; -*- -# Makefile for Sphinx documentation -# - -# You can set these variables from the command line. -SPHINXOPTS = -SPHINXBUILD = sphinx-build -PAPER = -BUILDDIR = _build - -# Internal variables. -PAPEROPT_a4 = -D latex_paper_size=a4 -PAPEROPT_letter = -D latex_paper_size=letter -ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . -# the i18n builder cannot share the environment and doctrees with the others -I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . - -.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext - -doc-help: - @echo "Please use \`make <target>' where <target> is one of" - @echo " html to make standalone HTML files" - @echo " dirhtml to make HTML files named index.html in directories" - @echo " singlehtml to make a single large HTML file" - @echo " pickle to make pickle files" - @echo " json to make JSON files" - @echo " htmlhelp to make HTML files and a HTML help project" - @echo " qthelp to make HTML files and a qthelp project" - @echo " devhelp to make HTML files and a Devhelp project" - @echo " epub to make an epub" - @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" - @echo " latexpdf to make LaTeX files and run them through pdflatex" - @echo " text to make text files" - @echo " man to make manual pages" - @echo " texinfo to make Texinfo files" - @echo " info to make Texinfo files and run them through makeinfo" - @echo " gettext to make PO message catalogs" - @echo " changes to make an overview of all changed/added/deprecated items" - @echo " linkcheck to check all external links for integrity" - @echo " doctest to run all doctests embedded in the documentation (if enabled)" - -doc-clean: - -rm -rf $(BUILDDIR)/* - -html: - $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html - @echo - @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." - -dirhtml: - $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml - @echo - @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." - -singlehtml: - $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml - @echo - @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." - -pickle: - $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle - @echo - @echo "Build finished; now you can process the pickle files." - -json: - $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json - @echo - @echo "Build finished; now you can process the JSON files." - -htmlhelp: - $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp - @echo - @echo "Build finished; now you can run HTML Help Workshop with the" \ - ".hhp project file in $(BUILDDIR)/htmlhelp." - -qthelp: - $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp - @echo - @echo "Build finished; now you can run "qcollectiongenerator" with the" \ - ".qhcp project file in $(BUILDDIR)/qthelp, like this:" - @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/Relcool.qhcp" - @echo "To view the help file:" - @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/Relcool.qhc" - -devhelp: - $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp - @echo - @echo "Build finished." - @echo "To view the help file:" - @echo "# mkdir -p $$HOME/.local/share/devhelp/Relcool" - @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/Relcool" - @echo "# devhelp" - -epub: - $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub - @echo - @echo "Build finished. The epub file is in $(BUILDDIR)/epub." - -latex: - $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex - @echo - @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." - @echo "Run \`make' in that directory to run these through (pdf)latex" \ - "(use \`make latexpdf' here to do that automatically)." - -latexpdf: - $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex - @echo "Running LaTeX files through pdflatex..." - $(MAKE) -C $(BUILDDIR)/latex all-pdf - @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." - -text: - $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text - @echo - @echo "Build finished. The text files are in $(BUILDDIR)/text." - -man: - $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man - @echo - @echo "Build finished. The manual pages are in $(BUILDDIR)/man." - -texinfo: - $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo - @echo - @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." - @echo "Run \`make' in that directory to run these through makeinfo" \ - "(use \`make info' here to do that automatically)." - -info: - $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo - @echo "Running Texinfo files through makeinfo..." - make -C $(BUILDDIR)/texinfo info - @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." - -gettext: - $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale - @echo - @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." - -changes: - $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes - @echo - @echo "The overview file is in $(BUILDDIR)/changes." - -linkcheck: - $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck - @echo - @echo "Link check complete; look for any errors in the above output " \ - "or in $(BUILDDIR)/linkcheck/output.txt." - -doctest: - $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest - @echo "Testing of doctests in the sources finished, look at the " \ - "results in $(BUILDDIR)/doctest/output.txt." diff --git a/docs/index.rst b/docs/index.rst deleted file mode 100644 index f367ef8..0000000 --- a/docs/index.rst +++ /dev/null @@ -1,22 +0,0 @@ -.. Relcool documentation master file, created by - sphinx-quickstart on Mon Sep 10 10:04:06 2012. - You can adapt this file completely to your liking, but it should at least - contain the root `toctree` directive. - -Welcome to Relcool's documentation! -=================================== - -Contents: - -.. toctree:: - :maxdepth: 2 - - - -Indices and tables -================== - -* :ref:`genindex` -* :ref:`modindex` -* :ref:`search` - diff --git a/docs/overview.rst b/docs/overview.rst deleted file mode 100644 index 77453a3..0000000 --- a/docs/overview.rst +++ /dev/null @@ -1,58 +0,0 @@ -Overview -======== - -Relcool is a tool that, given a specification, assembles -releases. That is it makes an assessment of all of the Erlang/OTP Apps -available to it and the constraints that you supply as part of your -configuration and resolves a complete set of OTP Applications and -thier versions for use in the release. This may be a bit hard to -understand if you are not familiar with the way version resolutions -work in package management systems. So lets look at an example. - -Lets say that you have the following OTP Applications - -* app1-1.2 - with dependencies:: - app2 - app5 - app6 -* app1-1.3 - with dependencies:: - app2 - app6 -* app2-2.0 - with dependencies:: - app6 -* app2-2.1 - with dependencies:: - app6 - app7 -* app3-2.0 -* app4-1.0.0 -* app5-3.0 -* app6-1.0 - with dependencies:: - app3 -* app7-2.0 - -This is the world of OTP Apps your Relcool knows about (basically OTP -Apps in the Library Directories you have specified). You have set a -config that looks like the following:: - - {release, {awesome_supercool, "1.0"}, - [{app1, "1.3", '>='}, - {app2, "2.0", '>'}, - app3]} - -When the Relcool process has run you will end up with a complete release as follows:: - - {release, {awesome_supercool, "1.0"}, - [{app1, "1.3"}, - {app2, "2.1"}, - {app3, "2.0"}, - {app6, "1.0"}, - {app7, "2.0"}]} - -As you can see that is a fully realied view of your direct and -transative dependencies based on the world that Relcool knows about -and the constraints that you specified in your configuration. diff --git a/examples/relcool.config b/examples/relcool.config new file mode 100644 index 0000000..fe6182b --- /dev/null +++ b/examples/relcool.config @@ -0,0 +1,74 @@ +%% -*- mode: Erlang; fill-column: 80; comment-column: 75; -*- +%% Example Relcool Config +%% ====================== +%% +%% This is an example relcool config whose purpose is to demonstrate all of the +%% options available in relcool. Its not expected that you will use all of the +%% things here. In fact, there is a high likely hood that *your* relcool.config +%% will be extremely minimal, as relcool does a very good job of figuring out +%% things on its own. +%% +%% The Release We Are Building +%% --------------------------- +%% +%% Lets say we have a release called sexpr. The sexpr release supports versions +%% 0.0.1 and 0.0.2 with different dependencies. 0.0.1 requires erlware commons +%% 0.8.0 or lesser. 0.0.2 requires erlware_commons 0.8.1 or greater along with +%% neotoma (any version). We also do not want neotoma to be loaded. We also want +%% our default release. the one we build in the common case to be sexper 0.0.2. + +%% You can tell relcool about additional directories that you want searched for +%% otp apps during the discovery process. You do that in the 'paths' config. You +%% can also specify these paths on the command line with `-p`. Be aware that +%% relcool plays well with rebar so if you have a deps directory in the current +%% directory it will be automatically added. +{paths, ["/opt/erlang_apps"]}. + + +%% If you have a sys.config file you need to tell relcool where it is. If you do +%% that relcool will include the sys.config in the appropriate place +%% automatically. +{sys_config, "./config/sys.config"}. + +%% relcool will include erts by default. However, if you don't want to include +%% erts you can add the `include_erts` tuple to the config and tell relcool not +%% to include it. +{include_erts, false}. + +%% When we have multiple releases relcool needs to know which one to build. You +%% can specify that on the command line with the `-n` and `-v` arguments to +%% relcool. However, it is often more convenient to do it in the config. +{default_release, sexpr, "0.0.2"}. + +{release, {sexpr, "0.0.1"}, + [sexpr, + %% There are two syntaxes for constraints. + %% The first is the tuple syntax shown here. + {erlware_commons, "0.8.0", '<='}]}. + +{release, {sexpr, "0.0.2"}, + [sexpr, + + %% This is the second constraint syntax, it is interchangeable with the tuple + %% syntax and its up to you which you find more readable/usable. + "erlware_commons>=0.8.1", + + %% You can put the release load types in the release spec here in exactly the + %% same way that you can do it for a normal relfile. The syntax is + %% {<constraint>, <type>}. + {neotoma, load}]}. + +%% During development its often the case that you want to substitute the app +%% that you are working on for a 'production' version of an app. You can +%% explicitly tell relcool to override all versions of an app that you specify +%% with an app in an arbitrary directory. Relcool will then symlink that app +%% into the release in place of the specified app. be aware though that relcool +%% will check your app for consistancy so it should be a normal OTP app and +%% already be built. +{overrides, [{sexpr, "../sexpr"}]}. + + +%% In some cases you might want to add additional functionality to relcool. You +%% can do this via a 'provider'. A provider is an implementation of the relcool +%% provider behaviour. This probably shouldn't be needed very often. +{add_providers, [my_custom_functionality]}. diff --git a/examples/relcool_simple.config b/examples/relcool_simple.config new file mode 100644 index 0000000..88e9e8d --- /dev/null +++ b/examples/relcool_simple.config @@ -0,0 +1,19 @@ +%% -*- mode: Erlang; fill-column: 80; comment-column: 75; -*- +%% Example Relcool Simple Config +%% ============================= +%% +%% This is an example relcool config whose purpose demonstrate the minimal +%% config needed for relcool. For a more complete example see the relcool.config +%% in the examples directory. +%% +%% The Release We Are Building +%% --------------------------- +%% +%% Lets say we have a release called sexpr. The sexpr release has version 0.0.2 +%% 0.0.2 requires erlware_commons 0.8.1 or greater along with neotoma +%% (any version). + +{release, {sexpr, "0.0.2"}, + [sexpr, + {erlware_commons, 0.8.1, '>='}, + neotoma]}. diff --git a/rebar.config b/rebar.config index 2211100..2b2f0b3 100644 --- a/rebar.config +++ b/rebar.config @@ -1,5 +1,5 @@ %% -*- mode: Erlang; fill-column: 80; comment-column: 75; -*- - +%% Dependencies ================================================================ {deps, [{rebar_vsn_plugin, ".*", {git, "https://github.com/erlware/rebar_vsn_plugin.git", {tag, "master"}}}, @@ -12,14 +12,21 @@ {git, "https://github.com/jcomellas/getopt.git", {branch, "master"}}}]}. +%% Rebar Plugins ============================================================== {plugins, [rebar_vsn_plugin]}. -{escript_incl_apps, - [getopt, erlware_commons]}. - -{first_files, [rcl_provider]}. +%% Compiler Options ============================================================ +{erl_opts, + [debug_info, warnings_as_errors, inline]}. +%% EUnit ======================================================================= +{eunit_opts, + [verbose, {report, {eunit_surefire, [{dir, "."}]}}]}. {cover_enabled, true}. {cover_print_enabled, true}. -{erl_opts, [debug_info, warnings_as_errors, inline]}. +%% Misc ======================================================================= +{escript_incl_apps, + [getopt, erlware_commons]}. + +{first_files, [rcl_provider]}. diff --git a/src/rcl_app_info.erl b/src/rcl_app_info.erl index d0192e6..bc64e30 100644 --- a/src/rcl_app_info.erl +++ b/src/rcl_app_info.erl @@ -49,6 +49,8 @@ active_deps/2, library_deps/1, library_deps/2, + link/1, + link/2, format_error/1, format/2, format/1]). @@ -60,6 +62,7 @@ -record(app_info_t, {name :: atom(), vsn :: ec_semver:semver(), dir :: file:name(), + link=false :: boolean(), active_deps :: [atom()], library_deps :: [atom()]}). @@ -80,7 +83,13 @@ new() -> %% @doc build a complete version of the app info with all fields set. -spec new(atom(), string(), file:name(), [atom()], [atom()]) -> {ok, t()} | relcool:error(). -new(AppName, Vsn, Dir, ActiveDeps, LibraryDeps) +new(AppName, Vsn, Dir, ActiveDeps, LibraryDeps) -> + new(AppName, Vsn, Dir, ActiveDeps, LibraryDeps, false). + +%% @doc build a complete version of the app info with all fields set. +-spec new(atom(), string(), file:name(), [atom()], [atom()], boolean()) -> + {ok, t()} | relcool:error(). +new(AppName, Vsn, Dir, ActiveDeps, LibraryDeps, Link) when erlang:is_atom(AppName), erlang:is_list(Dir), erlang:is_list(ActiveDeps), @@ -91,7 +100,8 @@ new(AppName, Vsn, Dir, ActiveDeps, LibraryDeps) ParsedVsn -> {ok, #app_info_t{name=AppName, vsn=ParsedVsn, dir=Dir, active_deps=ActiveDeps, - library_deps=LibraryDeps}} + library_deps=LibraryDeps, + link=Link}} end. -spec name(t()) -> atom(). @@ -145,6 +155,14 @@ library_deps(AppInfo=#app_info_t{}, LibraryDeps) when erlang:is_list(LibraryDeps) -> AppInfo#app_info_t{library_deps=LibraryDeps}. +-spec link(t()) -> boolean(). +link(#app_info_t{link=Link}) -> + Link. + +-spec link(t(), boolean()) -> t(). +link(AppInfo, NewLink) -> + AppInfo#app_info_t{link=NewLink}. + -spec format_error(Reason::term()) -> iolist(). format_error({vsn_parse, AppName}) -> io_lib:format("Error parsing version for ~p", @@ -156,9 +174,11 @@ format(AppInfo) -> -spec format(non_neg_integer(), t()) -> iolist(). format(Indent, #app_info_t{name=Name, vsn=Vsn, dir=Dir, - active_deps=Deps, library_deps=LibDeps}) -> + active_deps=Deps, library_deps=LibDeps, + link=Link}) -> [rcl_util:indent(Indent), erlang:atom_to_list(Name), "-", ec_semver:format(Vsn), ": ", Dir, "\n", + rcl_util:indent(Indent + 1), "Symlink: ", erlang:atom_to_list(Link), "\n", rcl_util:indent(Indent + 1), "Active Dependencies:\n", [[rcl_util:indent(Indent + 2), erlang:atom_to_list(Dep), ",\n"] || Dep <- Deps], rcl_util:indent(Indent + 1), "Library Dependencies:\n", diff --git a/src/rcl_cmd_args.erl b/src/rcl_cmd_args.erl index ab073a2..68bd9ce 100644 --- a/src/rcl_cmd_args.erl +++ b/src/rcl_cmd_args.erl @@ -147,7 +147,7 @@ convert_goals([RawSpec | Rest], Acc) -> -spec create_output_dir([getopt:option()], rcl_state:cmd_args()) -> {ok, rcl_state:cmd_args()} | relcool:error(). create_output_dir(Opts, Acc) -> - OutputDir = proplists:get_value(output_dir, Opts, "./relcool_output"), + OutputDir = proplists:get_value(output_dir, Opts, "./_rel"), case filelib:is_dir(OutputDir) of false -> case rcl_util:mkdir_p(OutputDir) of diff --git a/src/rcl_provider.erl b/src/rcl_provider.erl index 9d1dd88..c3ef434 100644 --- a/src/rcl_provider.erl +++ b/src/rcl_provider.erl @@ -10,6 +10,7 @@ %% API -export([new/2, do/2, + impl/1, format_error/1, format_error/2, format/1]). @@ -57,6 +58,12 @@ new(ModuleName, State0) when is_atom(ModuleName) -> do({?MODULE, Mod}, State) -> Mod:do(State). +%%% @doc get the name of the module that implements the provider +%%% @param Provider the provider object +-spec impl(Provider::t()) -> module(). +impl({?MODULE, Mod}) -> + Mod. + %% @doc format an error produced from a provider. -spec format_error(Reason::term()) -> iolist(). format_error({non_existing, ModuleName}) -> diff --git a/src/rcl_prv_assembler.erl b/src/rcl_prv_assembler.erl index a118b12..c0f65c9 100644 --- a/src/rcl_prv_assembler.erl +++ b/src/rcl_prv_assembler.erl @@ -73,8 +73,6 @@ format_error({release_script_generation_error, Module, Errors}) -> ["Errors generating release \n", rcl_util:indent(1), Module:format_error(Errors)]. - - %%%=================================================================== %%% Internal Functions %%%=================================================================== @@ -102,15 +100,20 @@ copy_app(LibDir, App) -> AppVsn = rcl_app_info:vsn_as_string(App), AppDir = rcl_app_info:dir(App), TargetDir = filename:join([LibDir, AppName ++ "-" ++ AppVsn]), - ec_plists:map(fun(SubDir) -> - copy_dir(AppDir, TargetDir, SubDir) - end, ["ebin", - "include", - "priv", - "src", - "c_src", - "README", - "LICENSE"]). + case rcl_app_info:link(App) of + true -> + file:make_symlink(AppDir, TargetDir); + false -> + ec_plists:map(fun(SubDir) -> + copy_dir(AppDir, TargetDir, SubDir) + end, ["ebin", + "include", + "priv", + "src", + "c_src", + "README", + "LICENSE"]) + end. copy_dir(AppDir, TargetDir, SubDir) -> SubSource = filename:join(AppDir, SubDir), @@ -157,7 +160,10 @@ write_bin_file(State, Release, OutputDir, RelDir) -> ok = ec_file:mkdir_p(BinDir), VsnRel = filename:join(BinDir, RelName ++ "-" ++ RelVsn), BareRel = filename:join(BinDir, RelName), - StartFile = bin_file_contents(RelName, RelVsn, rcl_release:erts(Release)), + ErlOpts = rcl_state:get(State, erl_opts, ""), + StartFile = bin_file_contents(RelName, RelVsn, + rcl_release:erts(Release), + ErlOpts), ok = file:write_file(VsnRel, StartFile), ok = file:change_mode(VsnRel, 8#777), ok = file:write_file(BareRel, StartFile), @@ -275,7 +281,7 @@ get_code_paths(Release, OutDir) -> rcl_app_info:vsn_as_string(App), "ebin"]) || App <- rcl_release:application_details(Release)]. -bin_file_contents(RelName, RelVsn, ErtsVsn) -> +bin_file_contents(RelName, RelVsn, ErtsVsn, ErlOpts) -> [<<"#!/bin/sh set -e @@ -286,6 +292,7 @@ REL_NAME=">>, RelName, <<" REL_VSN=">>, RelVsn, <<" ERTS_VSN=">>, ErtsVsn, <<" REL_DIR=$RELEASE_ROOT_DIR/releases/$REL_NAME-$REL_VSN +ERL_OPTS=">>, ErlOpts, <<" ERTS_DIR= SYS_CONFIG= @@ -324,6 +331,4 @@ export EMU=beam export PROGNAME=erl export LD_LIBRARY_PATH=$ERTS_DIR/lib - - -$BINDIR/erlexec $SYS_CONFIG -boot $REL_DIR/$REL_NAME $@">>]. +$BINDIR/erlexec $ERL_OPTS $SYS_CONFIG -boot $REL_DIR/$REL_NAME $@">>]. diff --git a/src/rcl_prv_config.erl b/src/rcl_prv_config.erl index 7a1cd19..ee1c770 100644 --- a/src/rcl_prv_config.erl +++ b/src/rcl_prv_config.erl @@ -82,7 +82,8 @@ load_terms({add_providers, Providers0}, {ok, State0}) -> ExistingProviders = rcl_state:providers(State1), {ok, rcl_state:providers(State1, ExistingProviders ++ Providers3)} end; - +load_terms({overrides, Overrides0}, {ok, State0}) -> + {ok, rcl_state:overrides(State0, Overrides0)}; load_terms({release, {RelName, Vsn}, Applications}, {ok, State0}) -> Release0 = rcl_release:new(RelName, Vsn), case rcl_release:goals(Release0, Applications) of diff --git a/src/rcl_prv_discover.erl b/src/rcl_prv_discover.erl index 23a3937..0a12d24 100644 --- a/src/rcl_prv_discover.erl +++ b/src/rcl_prv_discover.erl @@ -49,16 +49,28 @@ do(State) -> ["Resolving OTP Applications from directories:\n", [[rcl_util:indent(1), LibDir, "\n"] || LibDir <- LibDirs]] end), + resolve_app_metadata(State, LibDirs, OutputDir). +-spec format_error([ErrorDetail::term()]) -> iolist(). +format_error(ErrorDetails) + when erlang:is_list(ErrorDetails) -> + [[format_detail(ErrorDetail), "\n"] || ErrorDetail <- ErrorDetails]. + +%%%=================================================================== +%%% Internal Functions +%%%=================================================================== +resolve_app_metadata(State, LibDirs, OutputDir) -> AppMeta0 = lists:flatten(ec_plists:map(fun(LibDir) -> discover_dir([OutputDir], LibDir) end, LibDirs)), + AppMeta1 = setup_overrides(State, AppMeta0), + Errors = [case El of {error, Ret} -> Ret; _ -> El end - || El <- AppMeta0, + || El <- AppMeta1, case El of {error, _} -> true; @@ -66,30 +78,38 @@ do(State) -> false end], - lists:filter(fun({error, _}) -> true; - (_) -> false - end, AppMeta0), case Errors of [] -> - AppMeta1 = lists:flatten(AppMeta0), + AppMeta2 = lists:flatten(AppMeta1), rcl_log:debug(rcl_state:log(State), fun() -> ["Resolved the following OTP Applications from the system: \n", - [[rcl_app_info:format(1, App), "\n"] || App <- AppMeta1]] + [[rcl_app_info:format(1, App), "\n"] || App <- AppMeta2]] end), - {ok, rcl_state:available_apps(State, AppMeta1)}; + {ok, rcl_state:available_apps(State, AppMeta2)}; _ -> ?RCL_ERROR(Errors) end. --spec format_error([ErrorDetail::term()]) -> iolist(). -format_error(ErrorDetails) - when erlang:is_list(ErrorDetails) -> - [[format_detail(ErrorDetail), "\n"] || ErrorDetail <- ErrorDetails]. +app_name({error, _}) -> + undefined; +app_name(AppMeta) -> + rcl_app_info:name(AppMeta). + +setup_overrides(State, AppMetas0) -> + Overrides = rcl_state:overrides(State), + AppMetas1 = [AppMeta || AppMeta <- AppMetas0, + not lists:keymember(app_name(AppMeta), 1, Overrides)], + [case is_valid_otp_app(filename:join([FileName, "ebin", + erlang:atom_to_list(AppName) ++ ".app"])) of + [] -> + {error, {invalid_override, AppName, FileName}}; + Error = {error, _} -> + Error; + App -> + rcl_app_info:link(App, true) + end || {AppName, FileName} <- Overrides] ++ AppMetas1. -%%%=================================================================== -%%% Internal Functions -%%%=================================================================== get_lib_dirs(State) -> LibDirs0 = rcl_state:lib_dirs(State), add_rebar_deps_dir(State, LibDirs0). @@ -133,6 +153,9 @@ add_system_lib_dir(State, LibDirs) -> end. -spec format_detail(ErrorDetail::term()) -> iolist(). +format_detail({error, {invalid_override, AppName, FileName}}) -> + io_lib:format("Override {~p, ~p} is not a valid OTP App. Perhaps you forgot to build it?", + [AppName, FileName]); format_detail({accessing, File, eaccess}) -> io_lib:format("permission denied accessing file ~s", [File]); format_detail({accessing, File, Type}) -> @@ -172,7 +195,7 @@ discover_dir(IgnoreDirs, File) -> is_valid_otp_app(File) end. --spec is_valid_otp_app(file:name()) -> [rcl_app_info:t() | {error, Reason::term()} | []]. +-spec is_valid_otp_app(file:name()) -> rcl_app_info:t() | {error, Reason::term()} | []. is_valid_otp_app(File) -> %% Is this an ebin dir? EbinDir = filename:dirname(File), diff --git a/src/rcl_state.erl b/src/rcl_state.erl index 46c91c7..7397e6f 100644 --- a/src/rcl_state.erl +++ b/src/rcl_state.erl @@ -27,6 +27,8 @@ log/1, output_dir/1, lib_dirs/1, + overrides/1, + overrides/2, goals/1, config_files/1, providers/1, @@ -64,6 +66,7 @@ available_apps = [] :: [rcl_app_info:t()], default_release :: {rcl_release:name(), rcl_release:vsn()}, sys_config :: file:filename() | undefined, + overrides :: [{AppName::atom(), Directory::file:filename()}], releases :: ec_dictionary:dictionary({ReleaseName::atom(), ReleaseVsn::string()}, rcl_release:t()), @@ -96,10 +99,21 @@ new(PropList, Targets) when erlang:is_list(PropList) -> providers = [], releases=ec_dictionary:new(ec_dict), config_values=ec_dictionary:new(ec_dict), + overrides = proplists:get_value(overrides, PropList, []), default_release={proplists:get_value(relname, PropList, undefined), proplists:get_value(relvsn, PropList, undefined)}}, create_logic_providers(State0). +%% @doc the application overrides for the system +-spec overrides(t()) -> [{AppName::atom(), Directory::file:filename()}]. +overrides(#state_t{overrides=Overrides}) -> + Overrides. + +%% @doc the application overrides for the system +-spec overrides(t(), [{AppName::atom(), Directory::file:filename()}]) -> t(). +overrides(State, Overrides) -> + State#state_t{overrides=Overrides}. + %% @doc get the current log state for the system -spec log(t()) -> rcl_log:t(). log(#state_t{log=LogState}) -> diff --git a/src/relcool.erl b/src/relcool.erl index 44debc9..0d75c62 100644 --- a/src/relcool.erl +++ b/src/relcool.erl @@ -22,6 +22,7 @@ -export([main/1, do/7, + do/8, format_error/1, opt_spec_list/0]). @@ -59,15 +60,30 @@ main(Args) -> %% @param OutputDir - The directory where the release should be built to %% @param Configs - The list of config files for the system do(RelName, RelVsn, Goals, LibDirs, LogLevel, OutputDir, Configs) -> + do(RelName, RelVsn, Goals, LibDirs, LogLevel, OutputDir, [], Configs). + +%% @doc provides an API to run the Relcool process from erlang applications +%% +%% @param RelName - The release name to build (maybe `undefined`) +%% @param RelVsn - The release version to build (maybe `undefined`) +%% @param Goals - The release goals for the system in depsolver or Relcool goal +%% format +%% @param LibDirs - The library dirs that should be used for the system +%% @param OutputDir - The directory where the release should be built to +%% @param Overrides - A list of overrides for the system +%% @param Configs - The list of config files for the system +do(RelName, RelVsn, Goals, LibDirs, LogLevel, OutputDir, Overrides, Configs) -> State = rcl_state:new([{relname, RelName}, {relvsn, RelVsn}, {goals, Goals}, + {overrides, Overrides}, {output_dir, OutputDir}, {lib_dirs, LibDirs}, {log, rcl_log:new(LogLevel)}], Configs), run_relcool_process(rcl_state:caller(State, api)). + -spec opt_spec_list() -> [getopt:option_spec()]. opt_spec_list() -> [ @@ -112,14 +128,17 @@ run_providers(State0) -> {ok, State1} -> Providers = rcl_state:providers(State1), Result = run_providers(ConfigProvider, Providers, State1), - case rcl_state:caller(State1) of - command_line -> - init:stop(0); - api -> - Result - end + handle_output(State1, rcl_state:caller(State1), Result) end. +handle_output(State, command_line, E={error, _}) -> + report_error(State, E), + init:stop(127); +handle_output(_State, command_line, _) -> + init:stop(0); +handle_output(_State, api, Result) -> + Result. + run_providers(ConfigProvider, Providers, State0) -> case Providers of [ConfigProvider | Rest] -> @@ -135,10 +154,13 @@ run_providers(ConfigProvider, Providers, State0) -> run_provider(_Provider, Error = {error, _}) -> Error; run_provider(Provider, {ok, State0}) -> + rcl_log:debug(rcl_state:log(State0), "Running provider ~p~n", + [rcl_provider:impl(Provider)]), case rcl_provider:do(Provider, State0) of {ok, State1} -> {ok, State1}; E={error, _} -> + E end. diff --git a/test/rclt_release_SUITE.erl b/test/rclt_release_SUITE.erl index 7cdf362..c13672a 100644 --- a/test/rclt_release_SUITE.erl +++ b/test/rclt_release_SUITE.erl @@ -24,10 +24,12 @@ end_per_suite/1, init_per_testcase/2, all/0, - make_release/1]). + make_release/1, + make_overridden_release/1]). -include_lib("common_test/include/ct.hrl"). -include_lib("eunit/include/eunit.hrl"). +-include_lib("kernel/include/file.hrl"). suite() -> [{timetrap,{seconds,30}}]. @@ -47,7 +49,7 @@ init_per_testcase(_, Config) -> {state, State} | Config]. all() -> - [make_release]. + [make_release, make_overridden_release]. make_release(Config) -> LibDir1 = proplists:get_value(lib1, Config), @@ -84,6 +86,58 @@ make_release(Config) -> ?assert(lists:member({goal_app_2, "0.0.1"}, AppSpecs)), ?assert(lists:member({lib_dep_1, "0.0.1", load}, AppSpecs)). + +make_overridden_release(Config) -> + DataDir = proplists:get_value(data_dir, Config), + OverrideDir1 = filename:join([DataDir, create_random_name("override_dir_")]), + LibDir1 = proplists:get_value(lib1, Config), + [(fun({Name, Vsn}) -> + create_app(LibDir1, Name, Vsn, [kernel, stdlib], []) + end)(App) + || + App <- + [{create_random_name("lib_app1_"), create_random_vsn()} + || _ <- lists:seq(1, 100)]], + OverrideApp = create_random_name("override_app"), + OverrideVsn = create_random_vsn(), + OverrideAppDir = filename:join(OverrideDir1, OverrideApp), + OverrideAppName = erlang:list_to_atom(OverrideApp), + + create_app(LibDir1, "goal_app_1", "0.0.1", [stdlib,kernel,non_goal_1], []), + create_app(LibDir1, "lib_dep_1", "0.0.1", [stdlib,kernel], []), + create_app(LibDir1, "goal_app_2", "0.0.1", [stdlib,kernel,goal_app_1,non_goal_2], []), + create_app(LibDir1, "non_goal_1", "0.0.1", [stdlib,kernel], [lib_dep_1]), + create_app(LibDir1, "non_goal_2", "0.0.1", [stdlib,kernel], []), + + create_app(OverrideDir1, OverrideApp, OverrideVsn, [stdlib,kernel], []), + + ConfigFile = filename:join([LibDir1, "relcool.config"]), + write_config(ConfigFile, + [{release, {foo, "0.0.1"}, + [goal_app_1, + erlang:list_to_atom(OverrideApp), + goal_app_2]}]), + OutputDir = filename:join([proplists:get_value(data_dir, Config), + create_random_name("relcool-output")]), + {ok, State} = relcool:do(undefined, undefined, [], [LibDir1], 2, + OutputDir, [{OverrideAppName, OverrideAppDir}], + [ConfigFile]), + [{{foo, "0.0.1"}, Release}] = ec_dictionary:to_list(rcl_state:releases(State)), + AppSpecs = rcl_release:applications(Release), + ?assert(lists:keymember(stdlib, 1, AppSpecs)), + ?assert(lists:keymember(kernel, 1, AppSpecs)), + ?assert(lists:member({non_goal_1, "0.0.1"}, AppSpecs)), + ?assert(lists:member({non_goal_2, "0.0.1"}, AppSpecs)), + ?assert(lists:member({goal_app_1, "0.0.1"}, AppSpecs)), + ?assert(lists:member({goal_app_2, "0.0.1"}, AppSpecs)), + ?assert(lists:member({OverrideAppName, OverrideVsn}, AppSpecs)), + ?assert(lists:member({lib_dep_1, "0.0.1", load}, AppSpecs)), + {ok, Real} = file:read_link(filename:join([OutputDir, "lib", + OverrideApp ++ "-" ++ OverrideVsn])), + ?assertMatch(OverrideAppDir, Real). + + + %%%=================================================================== %%% Helper Functions %%%=================================================================== |