├── .travis.yml ├── 00-util.pl ├── 01-python.pl ├── 02-fs.pl ├── 03-homebrew.pl ├── 04-apt.pl ├── 05-git.pl ├── 06-meta.pl ├── 07-managed.pl ├── 08-pacman.pl ├── 09-freebsd.pl ├── LICENSE.md ├── Makefile ├── README.md ├── bootstrap.sh ├── bootstrap_dev.sh ├── marelle.pl ├── scripts ├── before_install.sh └── cibuild.sh ├── sudo.pl └── test_marelle.pl /.travis.yml: -------------------------------------------------------------------------------- 1 | language: prolog 2 | before_install: "scripts/before_install.sh" 3 | install: true 4 | script: "scripts/cibuild.sh" 5 | notifications: 6 | email: 7 | - lars@yencken.org 8 | -------------------------------------------------------------------------------- /00-util.pl: -------------------------------------------------------------------------------- 1 | % 2 | % util.pl 3 | % marelle-deps 4 | % 5 | % Utility methods common to multiple deps. 6 | % 7 | 8 | expand_path(Path0, Path) :- 9 | ( atom_concat('~/', Suffix, Path0) -> 10 | getenv('HOME', Home), 11 | join([Home, '/', Suffix], Path) 12 | ; 13 | Path = Path0 14 | ). 15 | 16 | isfile(Path0) :- 17 | expand_path(Path0, Path), 18 | exists_file(Path). 19 | 20 | isdir(Path0) :- 21 | expand_path(Path0, Path), 22 | exists_directory(Path). 23 | 24 | make_executable(Path) :- 25 | sh(['chmod a+x ', Path]). 26 | 27 | curl(Source, Dest) :- 28 | sh(['curl -s -o ', Dest, ' ', Source]). 29 | 30 | % sformat(+S0, +Vars, -S) is semidet. 31 | % String interpolation, where {} is replaced by an argument in the list. 32 | % Will fail if the number of {} is not the same as the number of vars passed 33 | % in. 34 | % 35 | % sformat('Hello ~a!', ['Bob'], 'Hello Bob!'). 36 | % 37 | sformat(S0, Vars, S) :- 38 | atomic_list_concat(Parts, '~a', S0), 39 | ( length(Vars, N), N1 is N + 1, length(Parts, N1) -> 40 | true 41 | ; 42 | throw('wrong number of arguments in interpolation') 43 | ), 44 | interleave(Parts, Vars, S1), 45 | atomic_list_concat(S1, '', S). 46 | 47 | interleave(Xs, Ys, Zs) :- 48 | ( Ys = [] -> 49 | Zs = Xs 50 | ; 51 | Ys = [Y|Yr], 52 | Xs = [X|Xr], 53 | Zs = [X, Y|Zr], 54 | interleave(Xr, Yr, Zr) 55 | ). 56 | -------------------------------------------------------------------------------- /01-python.pl: -------------------------------------------------------------------------------- 1 | % 2 | % 01-python-helpers.pl 3 | % marelle-deps 4 | % 5 | 6 | % python_pkg(-Pkg) is nondet. 7 | % Pkg is a python module imported using the same name. 8 | :- multifile python_pkg/1. 9 | 10 | % python_pkg(-Pkg, -ImportName) is nondet. 11 | % Pkg is a python module imported by a different name. 12 | :- multifile python_pkg/2. 13 | 14 | % pip_pkg(-Pkg) is nondet. 15 | % Pkg is a python module installable with pip. (satisfies met and meet 16 | % blocks) 17 | :- multifile pip_pkg/1. 18 | 19 | % pip_pkg(-Pkg, -PkgName) is nondet. 20 | % Pkg is a python module installable with pip. (satisfies met and meet 21 | % blocks) 22 | :- multifile pip_pkg/2. 23 | 24 | % pip_pkg(-Pkg, -PkgName, -PkgSource) is nondet. 25 | % Pkg is a python module installable with pip. (satisfies met and meet 26 | % blocks) 27 | :- multifile pip_pkg/3. 28 | 29 | % installs_with_pip(-Pkg) is nondet. 30 | % installs_with_pip(-Pkg, -PkgSource) is nondet. 31 | % Pkg is installed with pip as PkgSource. (satisfies meet block only) 32 | :- multifile installs_with_pip/2. 33 | :- multifile installs_with_pip/1. 34 | 35 | pip_pkg(P, P, P) :- pip_pkg(P). 36 | pip_pkg(P, PkgName, PkgName) :- pip_pkg(P, PkgName). 37 | 38 | % All python packages are packages. 39 | pkg(P) :- python_pkg(P, _). 40 | 41 | % If it's a Python package, it's met if you can import it. 42 | met(P, _) :- 43 | python_pkg(P, ImportName), !, 44 | python_import(ImportName). 45 | 46 | python_pkg(P, P) :- python_pkg(P). 47 | 48 | % python_import(+Pkg) is semidet. 49 | % Try to import the module in Python, failing if the import fails. 50 | python_import(Pkg) :- 51 | sh(['python -c \'import ', Pkg, '\' >/dev/null 2>/dev/null']). 52 | 53 | % All python packages depend on Python. 54 | depends(P, _, [python]) :- 55 | python_pkg(P). 56 | 57 | % All pip packages are also packages. 58 | pkg(P) :- pip_pkg(P, _, _). 59 | 60 | % all pip packages depend on pip 61 | depends(P, _, [pip]) :- pip_pkg(P, _, _). 62 | 63 | met(P, _) :- 64 | pip_pkg(P, PkgName, _), !, 65 | sh(['pip freeze 2>/dev/null | cut -d \'=\' -f 1 | fgrep -qi ', PkgName]). 66 | 67 | % all pip packages install using pip 68 | installs_with_pip(P, PkgSource) :- pip_pkg(P, _, PkgSource). 69 | installs_with_pip(P, P) :- installs_with_pip(P). 70 | 71 | % meet anything that installs with pip with by actually using pip 72 | meet(P, _) :- 73 | installs_with_pip(P, PkgSource), 74 | install_pip(PkgSource). 75 | 76 | % install_pip(+Pkg) is semidet. 77 | % Try to install the pacakge with pip, maybe using sudo. 78 | install_pip(Pkg) :- 79 | which(pip, Pip), 80 | atom_concat(Parent, '/pip', Pip), 81 | ( access_file(Parent, write) -> 82 | Sudo = '' 83 | ; 84 | Sudo = 'sudo ' 85 | ), 86 | join(['Installing ', Pkg, ' with pip'], Msg), 87 | writeln(Msg), 88 | sh(['umask a+rx && ', Sudo, 'pip install -U ', Pkg]). 89 | 90 | pkg(python). 91 | installs_with_brew(python). 92 | installs_with_apt(python, 'python-dev'). 93 | 94 | command_pkg(pip). 95 | meet(pip, linux(_)) :- 96 | install_apt('python-pip'). 97 | depends(pip, linux(_), ['build-essential']). 98 | 99 | pkg('build-essential'). 100 | installs_with_apt('build-essential'). 101 | 102 | depends(pip, _, [python]). 103 | -------------------------------------------------------------------------------- /02-fs.pl: -------------------------------------------------------------------------------- 1 | % 2 | % 02-fs.pl 3 | % marelle-deps 4 | % 5 | 6 | :- multifile symlink_step/3. 7 | 8 | pkg(P) :- symlink_step(P, _, _). 9 | met(P, _) :- 10 | symlink_step(P, Dest, Link), !, 11 | is_symlinked(Dest, Link). 12 | meet(P, _) :- 13 | symlink_step(P, Dest, Link), !, 14 | symlink(Dest, Link). 15 | 16 | % is_symlinked(+Dest, +Link) is semidet. 17 | % Check if a desired symlink already exists. 18 | is_symlinked(Dest0, Link0) :- 19 | expand_path(Dest0, Dest), 20 | expand_path(Link0, Link), 21 | read_link(Link, _, Dest). 22 | 23 | % symlink(+Dest, +Link) is semidet. 24 | % Create a symbolic link pointing to Dest. May fail if a file already 25 | % exists in the location Link. 26 | symlink(Dest0, Link0) :- 27 | expand_path(Dest0, Dest), 28 | expand_path(Link0, Link), 29 | sh(['ln -s ', Dest, ' ', Link]). 30 | -------------------------------------------------------------------------------- /03-homebrew.pl: -------------------------------------------------------------------------------- 1 | % 2 | % 03-homebrew.pl 3 | % marelle-deps 4 | % 5 | % Helpers for working with Homebrew. 6 | % http://mxcl.github.io/homebrew/ 7 | % 8 | 9 | command_pkg(brew). 10 | 11 | meet(brew, osx) :- 12 | sh('ruby -e "$(curl -fsSL https://raw.github.com/mxcl/homebrew/go)"'). 13 | 14 | % installs_with_brew(Pkg). 15 | % Pkg installs with homebrew package of same name. 16 | :- multifile installs_with_brew/1. 17 | 18 | % installs_with_brew(Pkg, BrewName). 19 | % Pkg installs with homebrew package called BrewName. 20 | :- multifile installs_with_brew/2. 21 | 22 | % installs_with_brew(Pkg, BrewName, Options). 23 | % Pkg installs with homebrew package called BrewName and with Options. 24 | :- multifile installs_with_brew/3. 25 | 26 | installs_with_brew(P, P) :- installs_with_brew(P). 27 | installs_with_brew(P, N, '') :- installs_with_brew(P, N). 28 | 29 | depends(P, osx, [brew, 'brew-update']) :- installs_with_brew(P, _). 30 | 31 | :- dynamic brew_updated/0. 32 | 33 | pkg('brew-update'). 34 | met('brew-update', osx) :- brew_updated. 35 | meet('brew-update', osx) :- 36 | sh('brew update'), 37 | assertz(brew_updated). 38 | 39 | met(P, osx) :- 40 | installs_with_brew(P, PkgName, _), !, 41 | join(['/usr/local/Cellar/', PkgName], Dir), 42 | isdir(Dir). 43 | 44 | meet(P, osx) :- 45 | installs_with_brew(P, PkgName, Options), !, 46 | install_brew(PkgName, Options). 47 | 48 | install_brew(Name, Options) :- 49 | sh(['brew install ', Name, ' ', Options]). 50 | 51 | % brew_tap(P, TapName). 52 | % An extra set of Homebrew packages. 53 | :- multifile brew_tap/2. 54 | 55 | % taps are targets 56 | pkg(P) :- brew_tap(P, _). 57 | 58 | met(P, osx) :- 59 | brew_tap(P, TapName), !, 60 | join(['/usr/local/Library/Taps/', TapName], Path), 61 | isdir(Path). 62 | 63 | meet(P, osx) :- 64 | brew_tap(P, TapName), !, 65 | sh(['brew tap ', TapName]). 66 | 67 | 68 | brew_tap('brew-cask-tap', 'caskroom/homebrew-cask'). 69 | pkg('brew-cask'). 70 | depends('brew-cask', osx, ['brew-cask-tap']). 71 | installs_with_brew('brew-cask'). 72 | 73 | pkg('brew-cask-configured'). 74 | depends('brew-cask-configured', osx, ['brew-cask']). 75 | met('brew-cask-configured', osx) :- isdir('/opt/homebrew-cask/Caskroom'). 76 | meet('brew-cask-configured', osx) :- sh('brew cask'). 77 | 78 | % installs_with_brew_cask(Pkg). 79 | % Pkg installs with homebrew-cask package of same name. 80 | :- multifile installs_with_brew_cask/1. 81 | 82 | % installs_with_brew_cask(Pkg, BrewName). 83 | % Pkg installs with homebrew-cask package called BrewName. 84 | :- multifile installs_with_brew_cask/2. 85 | 86 | % installs_with_brew_cask(Pkg, BrewName, Options). 87 | % Pkg installs with homebrew-cask package called BrewName and with Options. 88 | :- multifile installs_with_brew_cask/3. 89 | 90 | :- multifile cask_pkg/1. 91 | 92 | :- multifile cask_pkg/2. 93 | 94 | cask_pkg(P, P) :- cask_pkg(P). 95 | pkg(P) :- cask_pkg(P, _). 96 | installs_with_brew_cask(P, BrewName) :- cask_pkg(P, BrewName). 97 | installs_with_brew_cask(P, P) :- installs_with_brew_cask(P). 98 | installs_with_brew_cask(P, N, '') :- installs_with_brew_cask(P, N). 99 | depends(P, osx, ['brew-cask-configured', 'brew-update']) :- cask_pkg(P, _). 100 | 101 | met(P, osx) :- 102 | installs_with_brew_cask(P, PkgName, _), !, 103 | join(['/opt/homebrew-cask/Caskroom/', PkgName], Dir), 104 | isdir(Dir). 105 | 106 | meet(P, osx) :- 107 | installs_with_brew_cask(P, PkgName, Options), !, 108 | install_brew_cask(PkgName, Options). 109 | 110 | install_brew_cask(Name, Options) :- 111 | sh(['brew cask install ', Name, ' ', Options]). 112 | -------------------------------------------------------------------------------- /04-apt.pl: -------------------------------------------------------------------------------- 1 | % 2 | % 04-apt.pl 3 | % marelle-deps 4 | % 5 | 6 | % installs_with_apt(Pkg). 7 | % Pkg installs with apt package of same name on all Ubuntu/Debian flavours 8 | :- multifile installs_with_apt/1. 9 | 10 | % installs_with_apt(Pkg, AptName). 11 | % Pkg installs with apt package called AptName on all Ubuntu/Debian 12 | % flavours. AptName can also be a list of packages. 13 | :- multifile installs_with_apt/2. 14 | 15 | installs_with_apt(P, P) :- installs_with_apt(P). 16 | 17 | % installs_with_apt(Pkg, Codename, AptName). 18 | % Pkg installs with apt package called AptName on given Ubuntu/Debian 19 | % variant with given Codename. 20 | :- multifile installs_with_apt/3. 21 | 22 | installs_with_apt(P, _, AptName) :- installs_with_apt(P, AptName). 23 | 24 | depends(P, linux(_), ['apt-get-update']) :- 25 | isfile('/usr/bin/apt-get'), 26 | installs_with_apt(P, _, _). 27 | 28 | :- dynamic apt_updated/0. 29 | 30 | pkg('apt-get-update'). 31 | met('apt-get-update', linux(_)) :- 32 | isfile('/usr/bin/apt-get'), 33 | apt_updated. 34 | meet('apt-get-update', linux(_)) :- 35 | isfile('/usr/bin/apt-get'), 36 | sh('sudo apt-get update'), 37 | assertz(apt_updated). 38 | 39 | met(P, linux(Codename)) :- 40 | isfile('/usr/bin/apt-get'), 41 | installs_with_apt(P, Codename, PkgName), !, 42 | ( is_list(PkgName) -> 43 | maplist(check_dpkg, PkgName) 44 | ; 45 | check_dpkg(PkgName) 46 | ). 47 | 48 | meet(P, linux(Codename)) :- 49 | isfile('/usr/bin/apt-get'), 50 | installs_with_apt(P, Codename, PkgName), !, 51 | ( is_list(PkgName) -> 52 | maplist(install_apt, PkgName) 53 | ; 54 | install_apt(PkgName) 55 | ). 56 | 57 | check_dpkg(PkgName) :- 58 | join(['dpkg -s ', PkgName, ' >/dev/null 2>/dev/null'], Cmd), 59 | sh(Cmd). 60 | 61 | install_apt(Name) :- 62 | sudo_or_empty(Sudo), 63 | sh([Sudo, 'apt-get install -y ', Name]). 64 | -------------------------------------------------------------------------------- /05-git.pl: -------------------------------------------------------------------------------- 1 | % 2 | % 05-git.pl 3 | % marelle-deps 4 | % 5 | 6 | :- multifile git_step/3. 7 | 8 | pkg(P) :- git_step(P, _, _). 9 | 10 | met(P, _) :- 11 | git_step(P, _, Dest0), 12 | join([Dest0, '/.git'], Dest), 13 | isdir(Dest). 14 | 15 | meet(P, _) :- 16 | git_step(P, Repo, Dest0), 17 | expand_path(Dest0, Dest), 18 | git_clone(Repo, Dest). 19 | 20 | git_clone(Source, Dest) :- 21 | sh(['git clone --recursive ', Source, ' ', Dest]). 22 | -------------------------------------------------------------------------------- /06-meta.pl: -------------------------------------------------------------------------------- 1 | % 2 | % 06-meta.pl 3 | % marelle-deps 4 | % 5 | 6 | % meta_pkg(Name, Plat, Deps). 7 | % On platform Plat, you can set up Name by meeting Deps. 8 | :- multifile meta_pkg/3. 9 | 10 | 11 | % meta_pkg(Name, Deps). 12 | % On any platform, you can set up Name by meeting Deps. 13 | :- multifile meta_pkg/2. 14 | 15 | meta_pkg(P, _, Deps) :- meta_pkg(P, Deps). 16 | 17 | 18 | pkg(P) :- meta_pkg(P, _, _). 19 | 20 | met(P, Plat) :- meta_pkg(P, Plat, Deps), !, 21 | maplist(cached_met, Deps). 22 | 23 | meet(P, Plat) :- meta_pkg(P, Plat, _), !. 24 | 25 | depends(P, Plat, Deps) :- meta_pkg(P, Plat, Deps). 26 | -------------------------------------------------------------------------------- /07-managed.pl: -------------------------------------------------------------------------------- 1 | % 2 | % 07-managed.pl 3 | % marelle 4 | % 5 | % Shorthand for completely managed packages. 6 | % 7 | 8 | :- multifile managed_pkg/1. 9 | 10 | pkg(P) :- managed_pkg(P). 11 | 12 | installs_with_apt(P) :- managed_pkg(P). 13 | installs_with_brew(P) :- managed_pkg(P). 14 | installs_with_brew_cask(P) :- managed_pkg(P). 15 | installs_with_pacman(P) :- managed_pkg(P). 16 | installs_with_pkgng(P) :- managed_pkg(P). 17 | installs_with_ports(P) :- managed_pkg(P). 18 | -------------------------------------------------------------------------------- /08-pacman.pl: -------------------------------------------------------------------------------- 1 | % 2 | % 08-pacman.pl 3 | % marelle-deps 4 | % 5 | 6 | % installs_with_pacman(Pkg). 7 | % Pkg installs with pacman package of same name on Arch Linux 8 | :- multifile installs_with_pacman/1. 9 | 10 | % installs_with_pacman(Pkg, PacName). 11 | % Pkg installs with pacman package called PacName on Arch Linux 12 | % PacName can also be a list of packages. 13 | :- multifile installs_with_pacman/2. 14 | 15 | installs_with_pacman(P, P) :- installs_with_pacman(P). 16 | 17 | :- dynamic pacman_updated/0. 18 | 19 | pkg('pacman-update'). 20 | met('pacman-update', linux(arch)) :- pacman_updated. 21 | meet('pacman-update', linux(arch)) :- 22 | sh('sudo pacman -Syu'), 23 | assertz(pacman_updated). 24 | 25 | depends(P, linux(arch), ['pacman-update']) :- 26 | installs_with_pacman(P, _). 27 | 28 | % attempt to install a package with pacman 29 | install_pacman(Pkg) :- 30 | sudo_or_empty(Sudo), 31 | sh([Sudo, 'pacman -S --noconfirm ', Pkg]). 32 | 33 | % succeed only if the package is already installed 34 | check_pacman(Pkg) :- 35 | sh(['pacman -Qi ', Pkg, '>/dev/null 2>/dev/null']). 36 | 37 | met(P, linux(arch)) :- 38 | installs_with_pacman(P, PkgName), !, 39 | check_pacman(PkgName). 40 | 41 | meet(P, linux(arch)) :- 42 | installs_with_pacman(P, PkgName), !, 43 | install_pacman(PkgName). 44 | -------------------------------------------------------------------------------- /09-freebsd.pl: -------------------------------------------------------------------------------- 1 | % 2 | % 09-freebsd.pl 3 | % marelle 4 | % 5 | 6 | % installs_with_pkgng(Pkg). 7 | % Pkg installs with pkgng package of same name. 8 | :- multifile installs_with_pkgng/1. 9 | 10 | % installs_with_pkgng(Pkg, PkgName). 11 | % Pkg installs with pkgng package called PkgName. 12 | :- multifile installs_with_pkgng/2. 13 | 14 | % installs_with_ports(Pkg, PortName). 15 | % Pkg installs with FreeBSD port called PortName. 16 | :- multifile installs_with_ports/2. 17 | 18 | % installs_with_ports(Pkg, PortName, Options). 19 | % Pkg installs with FreeBSD port called PortName and with Options. 20 | :- multifile installs_with_ports/3. 21 | 22 | installs_with_pkgng(P, P) :- installs_with_pkgng(P). 23 | installs_with_ports(P, N, '') :- installs_with_ports(P, N). 24 | 25 | exists_pkgng(Name) :- sh(['pkg info ', Name, ' >/dev/null 2>/dev/null']). 26 | 27 | met(P, freebsd) :- 28 | ( installs_with_pkgng(P, PkgName) -> 29 | exists_pkgng(PkgName) 30 | ; installs_with_ports(P, PortName, _) -> 31 | exists_pkgng(PortName) 32 | ). 33 | 34 | meet(P, freebsd) :- 35 | ( installs_with_pkgng(P, PkgName) -> 36 | install_pkgng(PkgName) 37 | ; installs_with_ports(P, PortName, Options) -> 38 | install_ports(PortName, Options) 39 | ). 40 | 41 | install_pkgng(Name) :- 42 | sudo_or_empty(Sudo), 43 | sh([Sudo, 'pkg install -y ', Name]). 44 | 45 | install_ports(Name, Options) :- 46 | sudo_or_empty(Sudo), 47 | sh([Sudo, 'make BATCH=yes ', Options, ' -C/usr/ports/', Name, ' install clean']). 48 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013, Lars Yencken 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 5 | 6 | - Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 7 | - Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 8 | 9 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 10 | 11 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | test: 2 | exec swipl -q -t run_tests -s test_marelle.pl 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Marelle ("hopscotch") 2 | 3 | [![Build Status](https://travis-ci.org/larsyencken/marelle.png)](https://travis-ci.org/larsyencken/marelle) [![Gitter](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/larsyencken/marelle) 4 | 5 | 6 | Test-driven system administration in SWI-Prolog, in the style of [Babushka](https://github.com/benhoskings/babushka). 7 | 8 | Marelle uses [logic programming](https://en.wikipedia.org/wiki/Logic_programming) to describe system targets and rules by which these targets can be met. Prolog's built-in search mechanism makes writing and using these dependencies elegant. Anecdotally, writing deps for Marelle has the feel of teaching it about types of packages, rather than the feel of writing package templates. 9 | 10 | ![Hopscotch for Seniors](https://raw.github.com/wiki/larsyencken/marelle/img/HopscotchForSeniors.jpg) 11 | 12 | ## Current status 13 | 14 | Experimental but working. _Not actively maintained._ 15 | 16 | ## Features 17 | 18 | Marelle has some features common to other configuration management frameworks: 19 | 20 | - Checking and meeting dependencies (preconditions) 21 | - Testing whether a target installed correctly (post-conditions) 22 | - Ability to use platform-dependent instructions 23 | 24 | It also has some interesting differences: 25 | 26 | - Can write checks (`met` predicates) without needing to say how to meet them (`meet` predicates) 27 | - The dependencies of a target can vary by platform 28 | - Succinct definition of new classes of packages using logical rules 29 | 30 | ## Installing marelle 31 | 32 | ### Quickstart 33 | 34 | Pick a bootstrap script from the options below. If you're not sure, choose the stable version. 35 | 36 | Version | Bootstrap command 37 | ------- | ----------------- 38 | _0.1.0 (stable)_ | `bash -c "$(curl -fsSL https://raw.githubusercontent.com/larsyencken/marelle/versions/0.1.0/bootstrap.sh)"` 39 | _master (dev)_ | `bash -c "$(curl -fsSL https://raw.githubusercontent.com/larsyencken/marelle/master/bootstrap.sh)"` 40 | 41 | This will install marelle for all users, putting the executable in `/usr/local/bin/marelle`. 42 | 43 | ### Manual version 44 | 45 | 1. Get Prolog 46 | - On OS X, with Homebrew: `brew install swi-prolog` 47 | - On Ubuntu, with apt-get: `sudo apt-get install swi-prolog-nox` 48 | - On FreeBSD, with pkgng: `sudo pkg install swi-pl` 49 | 2. Get git 50 | - On OS X, with Homebrew: `brew install git` 51 | - On Ubuntu, with apt-get: `sudo apt-get install git` 52 | - On FreeBSD, with pkgng: `sudo pkg install git` 53 | 3. Clone and set up marelle 54 | 55 | ```bash 56 | # clone the repo 57 | mkdir -p ~/.local 58 | git clone https://github.com/larsyencken/marelle ~/.local/marelle 59 | 60 | # set up an executable in ~/.local/bin 61 | mkdir -p ~/.local/bin 62 | cat >~/.local/bin/marelle <>~/.profile 71 | source ~/.profile 72 | ``` 73 | 74 | ## Writing deps 75 | 76 | Make a `marelle-deps/` folder inside your project repo. Each package has two components, a `met/2` goal which checks if the dependency is met, and an `meet/2` goal with instructions on how to actually meet it if it's missing. 77 | 78 | For example, suppose I want to write a dep for Python that works on recent Ubuntu flavours. I might write: 79 | 80 | ```prolog 81 | % python is a target to meet 82 | pkg(python). 83 | 84 | % it's installed if it exists at /usr/bin/python 85 | met(python, linux(_)) :- exists_file('/usr/bin/python'). 86 | 87 | % we can install by running apt-get in shell 88 | meet(python, linux(_)) :- 89 | % could also use: install_apt('python-dev') 90 | sh('sudo apt-get install -y python-dev'). 91 | ``` 92 | 93 | To install python on a machine, I'd now run `marelle meet python`. 94 | 95 | To install pip, I might write: 96 | 97 | ```prolog 98 | pkg(pip). 99 | 100 | % pip is installed if we can run it 101 | met(pip, _) :- which(pip). 102 | 103 | % on all flavours of linux, try to install the python-pip package 104 | meet(pip, linux(_)) :- install_apt('python-pip'). 105 | 106 | % on all platforms, pip depends on python 107 | depends(pip, _, [python]). 108 | ``` 109 | Note our our use of platform specifiers and the `_` wildcard in their place. To see your current platform as described by marelle, run `marelle platform`. Examples include: `osx`, `linux(precise)` and `linux(raring)`. 110 | 111 | ## Running deps 112 | 113 | ### See available deps 114 | 115 | This runs every `met/2` statement that's valid for your platform. 116 | 117 | `marelle scan` 118 | 119 | ### Install something 120 | 121 | This will run the `meet/2` clause for your package, provided a valid one exists for your current platform. 122 | 123 | `marelle meet python` 124 | 125 | ### See your platform 126 | 127 | To find the right platform code to use in deps you're writing, run: 128 | 129 | `marelle platform` 130 | 131 | It reports the code for the platform you're currently on. 132 | 133 | ## Where to put your deps 134 | 135 | Like both Babushka and Babashka, Marelle looks for deps in `~/.marelle/deps` and in a folder called `marelle-deps` in the current directory, if either exists. This allows you to set up a personal set of deps for your environment, as well as project-specific deps. 136 | 137 | ## Examples 138 | 139 | See my [marelle-deps](https://github.com/larsyencken/marelle-deps) repo for working examples. 140 | 141 | ## Developing 142 | 143 | Run `make test` to run the test suite. 144 | -------------------------------------------------------------------------------- /bootstrap.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -e 2 | # 3 | # bootstrap.sh 4 | # 5 | # Install marelle for all users. 6 | # 7 | 8 | DEST_DIR=/usr/local/share/marelle 9 | DEST_BIN=/usr/local/bin/marelle 10 | 11 | function has_exec() { 12 | [ ! -z "$(which $1)" ] 13 | } 14 | 15 | function missing_exec() { 16 | [ -z "$(which $1)" ] 17 | } 18 | 19 | is_apt_updated=0 20 | function apt_update() { 21 | if [ $is_apt_updated -eq 0 ]; then 22 | sudo apt-get update 23 | is_apt_updated=1 24 | fi 25 | } 26 | 27 | is_brew_updated=0 28 | function brew_update() { 29 | if [ $is_brew_updated -eq 0 ]; then 30 | brew update 31 | is_brew_updated=1 32 | fi 33 | } 34 | 35 | function install_git() { 36 | echo 'Trying to install git' 37 | case $(uname -s) in 38 | Darwin) 39 | if has_exec brew; then 40 | brew_update 41 | brew install git 42 | else 43 | bail "Please install Homebrew and retry" 44 | fi 45 | ;; 46 | Linux) 47 | if has_exec apt-get; then 48 | apt_update 49 | sudo apt-get install -y git 50 | elif has_exec yum; then 51 | # XXX yum update? 52 | sudo yum install git 53 | else 54 | bail "Unknown linux variant" 55 | fi 56 | ;; 57 | FreeBSD) 58 | if has_exec pkg; then 59 | sudo pkg install -y git 60 | else 61 | bail "Old FreeBSD version without pkgng" 62 | fi 63 | ;; 64 | *) 65 | bail "Unknown operating system $(uname -s)" 66 | ;; 67 | esac 68 | } 69 | 70 | function install_prolog() { 71 | echo 'Trying to install prolog' 72 | case $(uname -s) in 73 | Darwin) 74 | if has_exec brew; then 75 | brew_update 76 | brew install swi-prolog 77 | else 78 | bail "Please install Homebrew and retry" 79 | fi 80 | ;; 81 | Linux) 82 | if has_exec apt-get; then 83 | apt_update 84 | sudo apt-get install -y swi-prolog-nox 85 | elif has_exec yum; then 86 | sudo yum install swi-prolog 87 | else 88 | bail "Unknown linux variant" 89 | fi 90 | ;; 91 | FreeBSD) 92 | if has_exec pkg; then 93 | sudo pkg install -y swi-pl 94 | else 95 | bail "Old FreeBSD version without pkgng" 96 | fi 97 | ;; 98 | *) 99 | bail "Unknown operating system $(uname -s)" 100 | ;; 101 | esac 102 | } 103 | 104 | function bail() 105 | { 106 | echo "$1 -- bailing" 107 | exit 1 108 | } 109 | 110 | function check_in_path() { 111 | echo $PATH | tr ':' '\n' | grep -x -c "$1"; 112 | } 113 | 114 | 115 | function checkout_marelle() { 116 | echo 'Trying to check out marelle' 117 | sudo mkdir -p "${DEST_DIR}" 118 | cd "$(dirname ${DEST_DIR})" 119 | sudo git clone -b versions/0.1.0 https://github.com/larsyencken/marelle 120 | sudo sh -c "cat > ${DEST_BIN}" <~/.local/bin/marelle <>~/.bash_profile 130 | source ~/.bash_profile 131 | elif [ -f ~/.profile ]; then 132 | echo 'export PATH=~/.local/bin:$PATH' >>~/.profile 133 | source ~/.profile 134 | fi 135 | if missing_exec marelle; then 136 | bail "Couldn't set up marelle in PATH. Add ~/.local/bin to your PATH in your shell's rc." 137 | fi 138 | } 139 | 140 | function main() { 141 | echo 'BOOTSTRAPPING MARELLE' 142 | 143 | if missing_exec git; then 144 | install_git 145 | fi 146 | echo 'Git: OK' 147 | 148 | if missing_exec swipl; then 149 | install_prolog 150 | fi 151 | echo 'Prolog: OK' 152 | 153 | if [ ! -d ~/.local/marelle ]; then 154 | checkout_marelle 155 | fi 156 | echo 'Marelle: OK' 157 | 158 | hash -r 159 | if missing_exec marelle; then 160 | put_marelle_in_path 161 | fi 162 | echo 'Marelle in PATH: OK' 163 | echo 'DONE' 164 | } 165 | 166 | main 167 | -------------------------------------------------------------------------------- /marelle.pl: -------------------------------------------------------------------------------- 1 | % 2 | % marelle 3 | % 4 | % Test driven system administration. 5 | % 6 | 7 | % 8 | % WRITING DEPS 9 | % 10 | % You need one each of these three statements. E.g. 11 | % 12 | % pkg(python). 13 | % met(python, _) :- which(python, _). 14 | % meet(python, osx) :- sh('brew install python'). 15 | % 16 | :- multifile pkg/1. 17 | :- multifile meet/2. 18 | :- multifile met/2. 19 | :- multifile depends/3. 20 | 21 | :- dynamic platform/1. 22 | 23 | marelle_version('dev'). 24 | 25 | % pkg(?Pkg) is nondet. 26 | % Is this a defined package name? 27 | 28 | % met(+Pkg, +Platform) is semidet. 29 | % Determine if the package is already installed. 30 | 31 | % meet(+Pkg, +Platform) is semidet. 32 | % Try to install this package. 33 | 34 | % where to look for dependencies 35 | marelle_search_path('~/.marelle/deps'). 36 | marelle_search_path('marelle-deps'). 37 | marelle_search_path('deps'). 38 | 39 | % 40 | % CORE CODE 41 | % 42 | % 43 | 44 | main :- 45 | ( current_prolog_flag(os_argv, Argv) -> 46 | true 47 | ; 48 | current_prolog_flag(argv, Argv) 49 | ), 50 | append([_, _, _, _, _, _], Rest, Argv), 51 | detect_platform, 52 | load_deps, 53 | ( Rest = [Command|SubArgs] -> 54 | main(Command, SubArgs) 55 | ; 56 | usage 57 | ). 58 | 59 | main(scan, Rest) :- 60 | ( Rest = ['--all'] -> 61 | scan_packages(all) 62 | ; Rest = ['--missing'] -> 63 | scan_packages(missing) 64 | ; Rest = [] -> 65 | scan_packages(unprefixed) 66 | ). 67 | 68 | main(list, Rest) :- 69 | ( Rest = [] ; Rest = [Pattern] ), 70 | !, 71 | ( Rest = [] -> 72 | findall(P, (pkg(P), \+ ishidden(P)), Ps0) 73 | ; Rest = [Pattern] -> 74 | join(['*', Pattern, '*'], Glob), 75 | findall(P, (pkg(P), wildcard_match(Glob, P), \+ ishidden(P)), Ps0) 76 | ), 77 | sort(Ps0, Ps), 78 | ( 79 | member(P, Ps), 80 | writeln(P), 81 | fail 82 | ; 83 | true 84 | ). 85 | 86 | main(met, [Pkg]) :- 87 | !, 88 | ( pkg(Pkg) -> 89 | ( met(Pkg) -> 90 | writeln('ok') 91 | ; 92 | writeln('not met'), 93 | fail 94 | ) 95 | ; 96 | join(['ERROR: ', Pkg, ' is not defined as a dep'], Msg), 97 | writeln(Msg), 98 | fail 99 | ). 100 | 101 | main(met, ['-q', Pkg]) :- !, met(Pkg). 102 | 103 | main(meet, Pkgs) :- !, maplist(meet_recursive, Pkgs). 104 | 105 | main(platform, []) :- !, platform(Plat), writeln(Plat). 106 | 107 | % start an interactive prolog shell 108 | main(debug, []) :- !, prolog. 109 | 110 | % run the command with profiling 111 | main(profile, [Cmd|Rest]) :- !, profile(main(Cmd, Rest)). 112 | 113 | % time the command and count inferences 114 | main(time, [Cmd|Rest]) :- !, time(main(Cmd, Rest)). 115 | 116 | main(version, []) :- 117 | marelle_version(V), writeln(V). 118 | 119 | main(_, _) :- !, usage. 120 | 121 | meet_recursive(Pkg) :- meet_recursive(Pkg, 0). 122 | 123 | meet_recursive(Pkg, Depth0) :- 124 | ( pkg(Pkg) -> 125 | ( cached_met(Pkg) -> 126 | join([Pkg, ' ✓'], M0), 127 | writeln_indent(M0, Depth0) 128 | ; ( join([Pkg, ' {'], M2), 129 | writeln_indent(M2, Depth0), 130 | force_depends(Pkg, Deps), 131 | Depth is Depth0 + 1, 132 | length(Deps, L), 133 | repeat_val(Depth, L, Depths), 134 | maplist(meet_recursive, Deps, Depths), 135 | meet(Pkg), 136 | cached_met(Pkg) 137 | ) -> 138 | join(['} ok ✓'], M4), 139 | writeln_indent(M4, Depth0) 140 | ; 141 | join(['} fail ✗'], M5), 142 | writeln_indent(M5, Depth0), 143 | fail 144 | ) 145 | ; 146 | join(['ERROR: ', Pkg, ' is not defined as a dep'], M6), 147 | writeln_indent(M6, Depth0), 148 | fail 149 | ). 150 | 151 | repeat_val(X, N, Xs) :- 152 | repeat_val(X, N, [], Xs). 153 | repeat_val(X, N0, Xs0, Xs) :- 154 | ( N0 = 0 -> 155 | Xs = Xs0 156 | ; 157 | N is N0 - 1, 158 | repeat_val(X, N, [X|Xs0], Xs) 159 | ). 160 | 161 | 162 | met(Pkg) :- 163 | platform(P), 164 | met(Pkg, P). 165 | 166 | meet(Pkg) :- 167 | platform(P), 168 | meet(Pkg, P). 169 | 170 | :- dynamic already_met/1. 171 | 172 | cached_met(Pkg) :- 173 | ( already_met(Pkg) -> 174 | true 175 | ; met(Pkg) -> 176 | assertz(already_met(Pkg)) 177 | ). 178 | 179 | % force_depends(+Pkg, -Deps) is det. 180 | % Get a list of dependencies for the given package on this platform. If 181 | % none exist, return an empty list. Supports multiple matching depends/3 182 | % statements for a package, or none. 183 | force_depends(Pkg, Deps) :- 184 | platform(P), 185 | findall(DepSet, depends(Pkg, P, DepSet), DepSets), 186 | flatten(DepSets, Deps0), 187 | list_to_set(Deps0, Deps). 188 | 189 | % scan_packages(+Visibility) is det. 190 | % Print all supported packages, marking installed ones with an asterisk. 191 | scan_packages(Visibility) :- 192 | writeln_stderr('Scanning packages...'), 193 | findall(P, package_state(P), Ps0), 194 | sort(Ps0, Ps1), 195 | ( Visibility = all -> 196 | Ps = Ps1 197 | ; Visibility = missing -> 198 | include(ismissing_ann, Ps1, Ps2), 199 | exclude(ishidden_ann, Ps2, Ps) 200 | ; 201 | exclude(ishidden_ann, Ps1, Ps) 202 | ), 203 | maplist(writepkg, Ps). 204 | 205 | ishidden(P) :- atom_concat('__', _, P). 206 | 207 | ishidden_ann(pkg(P, _)) :- ishidden(P). 208 | 209 | ismissing_ann(pkg(_, unmet)). 210 | 211 | % package_state(-Ann) is nondet 212 | % Find a package and it's current state as either met or unmet. 213 | package_state(Ann) :- 214 | pkg(Pkg), 215 | ground(Pkg), 216 | ( cached_met(Pkg) -> 217 | Ann = pkg(Pkg, met) 218 | ; 219 | Ann = pkg(Pkg, unmet) 220 | ). 221 | 222 | % load_deps is det. 223 | % Looks for dependency files to load from a per-user directory and from 224 | % a project specific directory. 225 | load_deps :- 226 | findall(P, ( 227 | marelle_search_path(P0), 228 | expand_path(P0, P), 229 | exists_directory(P) 230 | ), Ps), 231 | ( maplist(load_deps, Ps) -> 232 | true 233 | ; 234 | true 235 | ). 236 | 237 | load_deps(Dir) :- 238 | join([Dir, '/*.pl'], Pattern), 239 | expand_file_name(Pattern, Deps), 240 | load_files(Deps). 241 | 242 | usage :- 243 | writeln('Usage: marelle list [pattern]'), 244 | writeln(' marelle scan [--all | --missing]'), 245 | writeln(' marelle met [-q] '), 246 | writeln(' marelle meet '), 247 | writeln(' marelle platform'), 248 | writeln(' marelle version'), 249 | writeln(''), 250 | writeln('Detect and meet dependencies. Searches ~/.marelle/deps and the folder'), 251 | writeln('marelle-deps in the current directory if it exists.'). 252 | 253 | % which(+Command, -Path) is semidet. 254 | % See if a command is available in the current PATH, and return the path to 255 | % that command. 256 | which(Command, Path) :- 257 | sh_output(['which ', Command], Path). 258 | 259 | % which(+Command) is semidet. 260 | % See if a command is available in the current PATH. 261 | which(Command) :- which(Command, _). 262 | 263 | % platform(-Platform). 264 | % Determines the current platform (e.g. osx, ubuntu). Needs to be called 265 | % after detect_platform/0 has set the platform. 266 | platform(_) :- fail. 267 | 268 | % detect_platform is det. 269 | % Sets platform/1 with the current platform. 270 | detect_platform :- 271 | sh_output('uname -s', OS), 272 | ( OS = 'Linux' -> 273 | linux_name(Name), 274 | Platform = linux(Name) 275 | ; OS = 'Darwin' -> 276 | Platform = osx 277 | ; OS = 'FreeBSD' -> 278 | Platform = freebsd 279 | ; OS = 'OpenBSD' -> 280 | Platform = openbsd 281 | ; OS = 'NetBSD' -> 282 | Platform = netbsd 283 | ; 284 | Platform = unknown 285 | ), 286 | retractall(platform(_)), 287 | assertz(platform(Platform)). 288 | 289 | join(L, R) :- atomic_list_concat(L, R). 290 | 291 | % linux_name(-Name) is det. 292 | % Determine the codename of the linux release (e.g. precise). If there can 293 | % be no codename found, determine the short distro name (e.g. arch). 294 | % Otherwise codename is unknown. 295 | linux_name(Name) :- 296 | which('lsb_release', _), 297 | sh_output('lsb_release -c | sed \'s/^[^:]*:\\s//g\'', Name), 298 | dif(Name,'n/a'), !. 299 | linux_name(Name) :- 300 | which('lsb_release', _), 301 | sh_output('lsb_release -i | sed \'s/[A-Za-z ]*:\t//\'', CapitalName), 302 | dif(CapitalName,'n/a'), 303 | downcase_atom(CapitalName, Name), !. 304 | linux_name(unknown). 305 | 306 | 307 | writeln_indent(L, D) :- write_indent(D), writeln(L). 308 | writeln_star(L) :- write(L), write(' *\n'). 309 | write_indent(D) :- 310 | ( D = 0 -> 311 | true 312 | ; 313 | D1 is D - 1, 314 | write(' '), 315 | write_indent(D1) 316 | ). 317 | 318 | writepkg(pkg(P, met)) :- writeln_star(P). 319 | writepkg(pkg(P, unmet)) :- writeln(P). 320 | 321 | home_dir(D0, D) :- 322 | getenv('HOME', Home), 323 | join([Home, '/', D0], D). 324 | 325 | % command packages: met when their command is in path 326 | :- multifile command_pkg/1. 327 | :- multifile command_pkg/2. 328 | 329 | pkg(P) :- command_pkg(P, _). 330 | met(P, _) :- command_pkg(P, Cmd), which(Cmd). 331 | 332 | command_pkg(P, P) :- command_pkg(P). 333 | 334 | writeln_stderr(S) :- 335 | open('/dev/stderr', write, Stream), 336 | write(Stream, S), 337 | write(Stream, '\n'), 338 | close(Stream). 339 | 340 | join_if_list(Input, Output) :- 341 | ( is_list(Input) -> 342 | join(Input, Output) 343 | ; 344 | Output = Input 345 | ). 346 | 347 | % sh(+Cmd, -Code) is semidet. 348 | % Execute the given command in shell. Catch signals in the subshell and 349 | % cause it to fail if CTRL-C is given, rather than becoming interactive. 350 | % Code is the exit code of the command. 351 | sh(Cmd0, Code) :- 352 | join_if_list(Cmd0, Cmd), 353 | catch(shell(Cmd, Code), _, fail). 354 | 355 | bash(Cmd0, Code) :- sh(Cmd0, Code). 356 | 357 | % sh(+Cmd) is semidet. 358 | % Run the command in shell and fail unless it returns with exit code 0. 359 | sh(Cmd) :- sh(Cmd, 0). 360 | 361 | bash(Cmd0) :- sh(Cmd0). 362 | 363 | % sh_output(+Cmd, -Output) is semidet. 364 | % Run the command in shell and capture its stdout, trimming the last 365 | % newline. Fails if the command doesn't return status code 0. 366 | sh_output(Cmd0, Output) :- 367 | tmp_file(syscmd, TmpFile), 368 | join_if_list(Cmd0, Cmd), 369 | join([Cmd, ' >', TmpFile], Call), 370 | sh(Call), 371 | read_file_to_codes(TmpFile, Codes, []), 372 | atom_codes(Raw, Codes), 373 | atom_concat(Output, '\n', Raw). 374 | 375 | bash_output(Cmd, Output) :- sh_output(Cmd, Output). 376 | 377 | :- dynamic marelle_has_been_updated/0. 378 | 379 | pkg(selfupdate). 380 | met(selfupdate, _) :- marelle_has_been_updated. 381 | meet(selfupdate, _) :- 382 | sh('cd ~/.local/marelle && git pull'), 383 | assertz(marelle_has_been_updated). 384 | 385 | :- include('00-util'). 386 | :- include('01-python'). 387 | :- include('02-fs'). 388 | :- include('03-homebrew'). 389 | :- include('04-apt'). 390 | :- include('05-git'). 391 | :- include('06-meta'). 392 | :- include('07-managed'). 393 | :- include('08-pacman'). 394 | :- include('09-freebsd'). 395 | :- include('sudo'). 396 | -------------------------------------------------------------------------------- /scripts/before_install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | sudo apt-add-repository -y ppa:swi-prolog/stable 4 | sudo apt-get update 5 | sudo apt-get install -y swi-prolog 6 | -------------------------------------------------------------------------------- /scripts/cibuild.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | make test 4 | -------------------------------------------------------------------------------- /sudo.pl: -------------------------------------------------------------------------------- 1 | % 2 | % sudo.pl 3 | % marelle 4 | % 5 | 6 | % sudo_tell/1. 7 | % Like tell/1, but works on files which you need to sudo to get privileges 8 | % for. 9 | sudo_tell(Filename) :- 10 | expand_path(Filename, ExpFilename), 11 | join(['cat >', ExpFilename], BashCmd), 12 | which('sudo', Sudo), 13 | process_create( 14 | Sudo, 15 | ['/bin/sh', '-c', BashCmd], 16 | [stdin(pipe(Stream))] 17 | ), 18 | tell(Stream). 19 | 20 | % sudo_or_empty/1. 21 | % Returns an empty string when ran as root, path to sudo with 22 | % a trailing space otherwise. 23 | sudo_or_empty(Command) :- 24 | ( sh_output('whoami', root) -> 25 | Command = '' 26 | ; 27 | which('sudo', Sudo), 28 | join([Sudo, ' '], Command) 29 | ). 30 | 31 | sudo_sh(Command0) :- 32 | sudo_or_empty(Sudo), 33 | join_if_list(Command0, Command), 34 | tmp_file_stream(text, File, Stream), 35 | write(Stream, Command), 36 | close(Stream), 37 | sh([Sudo, 'sh "', File, '"']), 38 | delete_file(File). 39 | -------------------------------------------------------------------------------- /test_marelle.pl: -------------------------------------------------------------------------------- 1 | % 2 | % test_marelle.pl 3 | % marelle 4 | % 5 | % Unit tests for Marelle. 6 | % 7 | 8 | :- begin_tests(marelle). 9 | 10 | :- include('marelle'). 11 | 12 | test(sh) :- 13 | sh(echo), 14 | \+ sh('test 1 -eq 0'). 15 | 16 | test(isdir) :- 17 | isdir('.'), 18 | \+ isdir('7e1b960e8ccf8ed248d05f1803791da7'), 19 | sh('touch /tmp/7e1b960e8ccf8ed248d05f1803791da7'), 20 | \+ isdir('/tmp/7e1b960e8ccf8ed248d05f1803791da7'), 21 | sh('mkdir -p /tmp/2739b22b11ee348c6eda77b57c577485'), 22 | isdir('/tmp/2739b22b11ee348c6eda77b57c577485'). 23 | 24 | test(isfile) :- 25 | isfile('marelle.pl'), 26 | \+ isfile('.'), 27 | sh('mkdir -p /tmp/2739b22b11ee348c6eda77b57c577485'), 28 | \+ isfile('/tmp/2739b22b11ee348c6eda77b57c577485'), 29 | sh('touch /tmp/7e1b960e8ccf8ed248d05f1803791da7'), 30 | isfile('/tmp/7e1b960e8ccf8ed248d05f1803791da7'). 31 | 32 | test(sformat) :- 33 | sformat('', [], ''), 34 | sformat('Ohai', [], 'Ohai'), 35 | sformat('~a says hello', ['Bob'], 'Bob says hello'), 36 | sformat('Hello ~a', ['Bob'], 'Hello Bob'), 37 | sformat('~a, ~a, ~a', ['Once', 'twice', 'three times'], 38 | 'Once, twice, three times'), 39 | \+ catch( 40 | sformat('~a and ~a', [romeo], _), 41 | 'wrong number of arguments in interpolation', 42 | fail 43 | ). 44 | 45 | test(symlink) :- 46 | Dest = '/tmp/03435f97a0b2cef0780c9f2327e7e668', 47 | Link = '/tmp/03435f97a0b2cef0780c9f2327e7e668-link', 48 | sh(['touch ', Dest]), 49 | sh(['rm -f ', Link]), 50 | \+ is_symlinked(Dest, Link), 51 | symlink(Dest, Link), 52 | is_symlinked(Dest, Link). 53 | 54 | test(join) :- 55 | join([], ''), 56 | join([''], ''), 57 | join(['one'], 'one'), 58 | join(['one', ' two'], 'one two'), 59 | join(['one', ' two', ' and three'], 'one two and three'). 60 | 61 | :- end_tests(marelle). 62 | --------------------------------------------------------------------------------