diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml
new file mode 100644
index 0000000..8efae1f
--- /dev/null
+++ b/.github/FUNDING.yml
@@ -0,0 +1,2 @@
+github: fsquillace
+custom: https://github.com/fsquillace/junest/blob/master/README.md#donating
diff --git a/.gitignore b/.gitignore
index 1377554..b47a408 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1 +1,4 @@
*.swp
+*pkg.tar.*
+*.tar.gz
+*.SRCINFO
diff --git a/.travis.yml b/.travis.yml
new file mode 100644
index 0000000..fab23ba
--- /dev/null
+++ b/.travis.yml
@@ -0,0 +1,62 @@
+sudo: required
+os: linux
+
+cache:
+ directories:
+ - ~/.ccache
+ - ~/.pkg-cache
+
+services:
+- docker
+
+env:
+ matrix:
+ - TRAVIS_BASH_VERSION="4.0"
+
+before_install:
+ - ./ci/install-bash.sh "$TRAVIS_BASH_VERSION"
+ - sudo apt-get update
+ - sudo apt-get -y install awscli
+
+install:
+ - PATH=$PWD/bin:$PATH
+
+script:
+ #######################
+ # Unit Tests
+ #######################
+ - bash --version
+ - bash ./tests/checkstyle/checkstyle.sh
+ - bash ./tests/unit-tests/unit-tests.sh
+ - shellcheck bin/junest lib/**/*.sh ci/*.sh tests/**/*.sh
+
+ # ARM with qemu does seem to work properly. Disabling integ tests for ARM for now.
+ #- export JUNEST_HOME=~/.junest-arm
+ #- junest setup --arch arm
+ #- junest proot --fakeroot -- echo "Installing JuNest (\$(uname -m))"
+ #- junest proot --fakeroot -- ${PWD}/lib/checks/check.sh --skip-aur-tests
+ #- junest proot -- ${PWD}/lib/checks/check.sh --skip-aur-tests --use-sudo
+ #- yes | junest setup --delete
+
+ #######################
+ # Build and validation
+ #######################
+ - echo "$DOCKER_PASSWORD" | docker login --username "$DOCKER_USERNAME" --password-stdin
+ - docker run --rm -v "$(pwd):/build" -v ~/.ccache:/home/travis/.ccache -v ~/.pkg-cache:/var/cache/pacman/pkg --privileged archlinux:latest bash /build/ci/build_image.sh
+
+ - "echo pacman pkg cache size: $(du -h ~/.pkg-cache|cut -f1) in $(ls ~/.pkg-cache|wc -l) files"
+ - ls -l
+ # Test the newly created JuNest image against Ubuntu host
+ - export JUNEST_HOME=~/.junest
+ - junest setup -i junest-x86_64.tar.gz
+ # TODO The check does not work at the moment: https://app.travis-ci.com/github/fsquillace/junest/builds/271706037
+ # Disabling it in order to avoid having stale version of junest images.
+ - ${PWD}/lib/checks/check_all.sh
+ - yes | junest setup --delete
+
+
+after_success:
+ #######################
+ # Deploy and validation
+ #######################
+ - ./ci/deploy.sh ./junest-x86_64.tar.gz
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 0000000..9ea8193
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,260 @@
+Contributing to JuNest
+=====================
+
+First off, thanks for taking the time to contribute!
+
+The following is a set of guidelines for contributing to JuNest.
+These are just guidelines, not rules, use your best judgment and
+feel free to propose changes to this document in a pull request.
+
+**Table of Contents**
+
+- [How Can I Contribute?](#how-can-i-contribute)
+ - [Reporting Bugs](#reporting-bugs)
+ - [Suggesting Enhancements](#suggesting-enhancements)
+ - [Your First Code Contribution](#your-first-code-contribution)
+
+- [Styleguides](#styleguides)
+ - [Git Commit Messages](#git-commit-messages)
+ - [Documentation Styleguide](#documentation-styleguide)
+ - [Shell Styleguide](#shell-styleguide)
+
+- [Versioning](#versioning)
+
+## How Can I Contribute? ##
+
+### Reporting Bugs ###
+
+This section guides you through submitting a bug report for JuNest.
+
+#### Before submitting a bug report ####
+
+You might be able to find the cause of the problem and fix things yourself.
+
+- **Check the [troubleshooting section](https://github.com/fsquillace/junest#troubleshooting)**
+- **Check if you can reproduce the problem with the latest version of JuNest**
+- **Check for [existing open/closed issues](https://github.com/fsquillace/junest/issues?utf8=%E2%9C%93&q=is%3Aissue)**
+ - If the bug has already been suggested, add a comment to the existing issue instead of opening a new one.
+
+#### How Do I Submit A (Good) Bug Report? ####
+
+Bugs are tracked as [GitHub issues](https://guides.github.com/features/issues/) in the [JuNest issues page](https://github.com/fsquillace/junest/issues).
+Explain the problem and include additional details to help maintainers reproduce the problem:
+
+* **Use a clear and descriptive title** for the issue to identify the problem.
+* **Describe the exact steps which reproduce the problem** in as many details as possible. For example, start by explaining how you started JuNest, e.g. which command exactly you used in the terminal. When listing steps, **don't just say what you did, but explain how you did it**. For example.
+* **Provide specific examples to demonstrate the steps**. Include links to files or GitHub projects, or copy/pasteable snippets, which you use in those examples. If you're providing snippets in the issue, use [Markdown code blocks](https://help.github.com/articles/markdown-basics/#multiple-lines).
+* **Describe the behavior you observed after following the steps** and point out what exactly is the problem with that behavior.
+* **Explain which behavior you expected to see instead and why.**
+* **Put the bug label to the issue.**
+
+Include details about your configuration and environment:
+
+* **Which version of JuNest are you using?**
+* **What's the name and version of the OS you're using**?
+* **Are you running JuNest in a virtual machine?** If so, which VM software are you using and which operating systems and versions are used for the host and the guest?
+* **Which packages do you have installed?** You can get that list by running `pacman -Qq`.
+
+#### Template For Submitting Bug Reports ####
+
+ [Short description of problem here]
+
+ **Reproduction Steps:**
+
+ 1. [First Step]
+ 2. [Second Step]
+ 3. [Other Steps...]
+
+ **Expected behavior:**
+
+ [Describe expected behavior here]
+
+ **Observed behavior:**
+
+ [Describe observed behavior here]
+
+ **JuNest version:** [Enter JuNest version here]
+ **OS and version:** [Enter OS name and version here]
+
+ **Installed packages:**
+
+ [List of installed packages here]
+
+ **Additional information:**
+
+ * Problem started happening recently, didn't happen in an older version of JuNest: [Yes/No]
+ * Problem can be reliably reproduced, doesn't happen randomly: [Yes/No]
+
+### Suggesting Enhancements ###
+
+This section guides you through submitting an enhancement suggestion for JuNest, including completely new features and minor improvements to existing functionality.
+
+#### Before Submitting An Enhancement Suggestion ####
+
+* **Check if you're using the latest version of JuNest**
+- **Check for [existing open/closed issues](https://github.com/fsquillace/junest/issues?utf8=%E2%9C%93&q=is%3Aissue)**
+ - If enhancement has already been suggested, add a comment to the existing issue instead of opening a new one.
+
+#### How Do I Submit A (Good) Enhancement Suggestion? ####
+
+Enhancement suggestions are tracked as [GitHub issues](https://guides.github.com/features/issues/) in the [JuNest issues page](https://github.com/fsquillace/junest/issues).
+
+Create an issue on that repository and provide the following information:
+
+* **Use a clear and descriptive title** for the issue to identify the suggestion.
+* **Provide a step-by-step description of the suggested enhancement** in as many details as possible.
+* **Provide specific examples to demonstrate the steps**. Include copy/pasteable snippets which you use in those examples, as [Markdown code blocks](https://help.github.com/articles/markdown-basics/#multiple-lines).
+* **Describe the current behavior** and **explain which behavior you expected to see instead** and why.
+* **Specify which version of JuNest you're using.**
+* **Specify the name and version of the OS you're using.**
+* **Put the enanchement label to the issue.**
+
+#### Template For Submitting Enhancement Suggestions ####
+
+ [Short description of suggestion]
+
+ **Steps which explain the enhancement**
+
+ 1. [First Step]
+ 2. [Second Step]
+ 3. [Other Steps...]
+
+ **Current and suggested behavior**
+
+ [Describe current and suggested behavior here]
+
+ **Why would the enhancement be useful to most users**
+
+ [Explain why the enhancement would be useful to most users]
+
+ [List some other text editors or applications where this enhancement exists]
+
+ **JuNest Version:** [Enter JuNest version here]
+ **OS and Version:** [Enter OS name and version here]
+
+### Your First Code Contribution ###
+
+All JuNest issues are tracked as [GitHub issues](https://guides.github.com/features/issues/) in the [JuNest issues page](https://github.com/fsquillace/junest/issues).
+
+#### Pull Requests ####
+
+* Fork the repo and create your feature branch from ***dev***.
+* If you make significant changes, please add tests too.
+ Get familiar with [shunit](https://github.com/kward/shunit2).
+* If you've changed APIs, please update the documentation
+* Follow the [Shell styleguide](#shell-styleguide).
+* Document new code based on the
+ [Documentation Styleguide](#documentation-styleguide).
+* End files with a newline.
+* Follow the [Git commit messages](#git-commit-messages).
+* Send a [GitHub Pull Request to JuNest](https://github.com/fsquillace/junest/compare/dev...) with a clear list of what you've done (read more about [pull requests](http://help.github.com/pull-requests/)).
+* Put **dev as the base branch** and NOT the master one.
+
+#### Unit Tests ####
+To run unit tests:
+```sh
+./tests/unit-tests/unit-tests.sh
+```
+
+#### Integration Tests ####
+Generally, there is no need to run integration tests locally
+since [Travis](https://travis-ci.org/fsquillace/junest) will run as
+soon as the pull request gets created.
+
+## Styleguides ##
+
+### Git Commit Messages ###
+
+* Follow the [seven rules](http://chris.beams.io/posts/git-commit/#seven-rules) of a great Git commit message
+* Reference issues and pull requests liberally
+* Consider starting the commit message with an applicable emoji:
+ * :art: `:art:` when improving the format/structure of the code
+ * :racehorse: `:racehorse:` when improving performance
+ * :non-potable_water: `:non-potable_water:` when plugging memory leaks
+ * :memo: `:memo:` when writing docs
+ * :penguin: `:penguin:` when fixing something on Linux
+ * :apple: `:apple:` when fixing something on Mac OS
+ * :checkered_flag: `:checkered_flag:` when fixing something on Windows
+ * :bug: `:bug:` when fixing a bug
+ * :fire: `:fire:` when removing code or files
+ * :green_heart: `:green_heart:` when fixing the CI build
+ * :white_check_mark: `:white_check_mark:` when adding tests
+ * :lock: `:lock:` when dealing with security
+ * :arrow_up: `:arrow_up:` when upgrading dependencies
+ * :arrow_down: `:arrow_down:` when downgrading dependencies
+ * :shirt: `:shirt:` when removing linter warnings
+ * :package: `:package:` when bumping the version
+
+### Documentation Styleguide ###
+
+* Use [Markdown](https://daringfireball.net/projects/markdown).
+
+### Shell Styleguide ###
+
+* Use [google shell styleguide](https://google.github.io/styleguide/shell.xml)
+
+#### Function documentation ####
+For function documentation follows the example below:
+
+ #######################################
+ # Cleanup files from the backup dir.
+ #
+ # Globals:
+ # VAR1 (RO,bool) : `my_func` access to VAR1.
+ # VAR2 (WO) : `my_func` change the value of VAR2.
+ # VAR3 (RW) : `my_func` read and write to VAR3.
+ # Arguments:
+ # arg1 ($1,int) : Directory to cleanup.
+ # arg2 ($2-) : Command to execute for the cleanup.
+ # Returns:
+ # 0 : Cleanup completed successfully.
+ # 101 : Backup directory is not readable.
+ # $NOT_DIR_ERROR : Backup directory is not a directory.
+ # Output:
+ # None
+ #######################################
+ my_func() {
+ local arg1=$1
+ shift
+ local arg2=$@
+ ...
+ }
+
+The documentation is divided by a description of the function, a `Globals`,
+`Arguments`, `Returns` and `Output` sections. If a section does not need details
+use the word `None` inside of it.
+
+`Globals` section provides all global variables that interact with the function.
+
+`Arguments` section provides the list of arguments to pass to the function. Use
+the parenthesis to indicate the position of the arguments:
+
+- `$1` : Argument is in position one.
+- `$2-` : Argument takes all args from position two up to the end.
+- `$@` : Argument takes all args.
+- `$3?` : Argument is optional.
+
+Variables defined in `Globals` and `Arguments` sections can have the following
+types:
+
+- `int` : Integer variable.
+- `str` : String variable (default).
+- `bool` : Bool variable.
+
+`Returns` section contains the exit status of the function.
+
+`Output` section describe the string printed to stdout.
+
+## Versioning ##
+
+* JuNest uses the following [semantic versioning](http://semver.org/)
+
+### Public API ###
+
+The public API refers to the following parts of JuNest system:
+
+- JuNest script CLI
+
+Any potential code change that cause backwards incompatible changes to the
+public API requires the major version to be incremented.
+
diff --git a/LICENSE b/LICENSE
index 6b156fe..610ce97 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,4 +1,6 @@
-GNU GENERAL PUBLIC LICENSE
+
+
+ GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc.
@@ -631,8 +633,8 @@ to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
- {one line to give the program's name and a brief idea of what it does.}
- Copyright (C) {year} {name of author}
+ JuNest: The Arch Linux based distro that runs upon any Linux distros without root access
+ Copyright (C) 2014 Filippo Squillace
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
@@ -652,7 +654,7 @@ Also add information on how to contact you by electronic and paper mail.
If the program does terminal interaction, make it output a short
notice like this when it starts in an interactive mode:
- {project} Copyright (C) {year} {fullname}
+ JuNest Copyright (C) 2014 Filippo Squillace
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
@@ -673,3 +675,4 @@ the library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License. But first, please read
.
+
diff --git a/README.md b/README.md
index a492cbf..1268f8b 100644
--- a/README.md
+++ b/README.md
@@ -1,179 +1,616 @@
-JuJu
-====
-**JuJu**: the GNU/Linux distribution container for non-root users
+JuNest
+======
+
+> [!IMPORTANT]
+> Starting from Ubuntu 23.10+, [unprivileged user namespaces has been restricted](https://ubuntu.com/blog/ubuntu-23-10-restricted-unprivileged-user-namespaces).
+> If using JuNest within Ubuntu, you may need root privileges in order to enable it.
+> Alternatively, you can access JuNest using the `proot` mode as described
+> [below](#Proot-based).
+
+The lightweight Arch Linux based distro that runs, without root privileges, on top of any other Linux distro.
+
+
+
+
+
+|Project Status|Donation|Communication|
+|:------------:|:------:|:-----------:|
+| [](https://app.travis-ci.com/github/fsquillace/junest) [](https://www.openhub.net/p/junest) | [](https://github.com/sponsors/fsquillace) [](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=8LEHQKBCYTACY) [](https://www.buymeacoffee.com/fsquillace) | [](https://discord.gg/ttfBT7MKve) |
+
+**Table of Contents**
+- [Description](#description)
+- [Quickstart](#quickstart)
+- [Installation](#installation)
+- [Usage](#usage)
+- [Advanced usage](#advanced-usage)
+- [Internals](#internals)
+- [Troubleshooting](#troubleshooting)
+- [More documentation](#more-documentation)
+- [Contributing](#contributing)
+- [Donating](#donating)
+- [Authors](#authors)
Description
------------
-**JuJu** is a small ArchLinux based GNU/Linux distribution.
+===========
+**JuNest** (Jailed User Nest) is a lightweight Arch Linux based distribution
+that allows the creation of disposable and partially isolated GNU/Linux environments
+within any generic GNU/Linux host OS and without requiring root
+privileges to install packages.
-It allows to have an isolated GNU/Linux environment inside any generic host GNU/Linux OS
-and without the need to have root privileges for installing packages.
+JuNest is built around [pacman](https://wiki.archlinux.org/index.php/Pacman),
+the Arch Linux package manager, which allows access
+to a wide range of packages from the Arch Linux repositories.
-JuJu contains just the package managers (called pacman and yaourt) that allows to access
-to a wide range of packages from ArchLinux repositories.
+The main advantages of using JuNest include:
-The main advantages on using JuJu are:
- Install packages without root privileges.
-- Isolated environment in which you can install packages without affecting a production system.
-- Access to a wide range of packages in particular on GNU/Linux distros that may contain a limited repositories (such as CentOS and RedHat).
-- Available for x86\_64, i686 and ARMv6 architectures but you can build you own image from scratch too!
-- All ArchLinux lovers can have their favourite distro everywhere!
+- Create partially isolated environments in which you can install packages without risking mishaps on production systems.
+- Access a wider range of packages, particularly on GNU/Linux distros with comparatively limited repositories (such as CentOS and Red Hat).
+- Run on a different architecture from the host OS via QEMU.
+- Available for `x86_64` and `arm` architectures but you can build your own image from scratch too!
+- All Arch Linux lovers can enjoy their favourite distro everywhere!
-Quickstart
-----------
-There are three different ways you can run JuJu:
+JuNest follows the [Arch Linux philosophy](https://wiki.archlinux.org/index.php/The_Arch_Way).
-- As normal user - Allow to make basic operations using [proot](https://wiki.archlinux.org/index.php/Proot):
-```
-$ juju
-```
-- As fakeroot - Allow to install/remove packages using [proot](https://wiki.archlinux.org/index.php/Proot):
-```
-$ juju -f
-```
-- As root - Allow to have fully root privileges inside JuJu environment using [arch-chroot](https://wiki.archlinux.org/index.php/Chroot) (you need to be root for executing this):
-```
-# juju -r
-```
+How different is JuNest from Docker and Vagrant?
+------------------------------------------------
+Although JuNest sounds similar to a virtualisation/Linux container-like system,
+JuNest is quite different from solutions like Docker or Vagrant.
+In fact, the purpose of JuNest is **not** to
+build a completely isolated environment but, conversely, to provide the ability to run
+programs as if they were running natively from the host OS. Almost everything is shared
+between the host OS and the JuNest sandbox (kernel, process subtree, network, mounting, etc)
+and only the root filesystem gets isolated
+(since the programs installed in JuNest need to reside elsewhere).
-The first time you execute it, the script will download the JuJu image and place it
-to the default directory ~/.juju.
-You can change the default directory by changing the environment variable *JUJU\_HOME*.
+This allows interaction between processes belonging to both host OS and JuNest.
+For example, you can install the `top` command in JuNest and use it to monitor
+processes belonging to the host OS.
Installation
+============
+
+## Dependencies ##
+JuNest comes with a very short list of dependencies in order to be installed in most
+of GNU/Linux distributions.
+Before installing JuNest be sure that all dependencies are properly installed in your system:
+
+- [bash (>=4.0)](https://www.gnu.org/software/bash/)
+- [GNU coreutils](https://www.gnu.org/software/coreutils/)
+
+## Installation from git repository ##
+Just clone the JuNest repo somewhere (for example in ~/.local/share/junest):
+
+```sh
+git clone https://github.com/fsquillace/junest.git ~/.local/share/junest
+export PATH=~/.local/share/junest/bin:$PATH
+```
+
+Optionally you want to use the wrappers to run commands
+installed in JuNest directly from host:
+
+```sh
+export PATH="$PATH:~/.junest/usr/bin_wrappers"
+```
+Update your `~/.bashrc` or `~/.zshrc` to get always the wrappers available.
+
+### Installation using AUR (Arch Linux only) ###
+If you are using an Arch Linux system you can, alternatively, install JuNest from the [AUR repository](https://aur.archlinux.org/packages/junest-git/).
+JuNest will be located in `/opt/junest/`
+
+Quickstart
+==========
+
+Setup environment
+-----------------
+
+The first operation required is to install the JuNest environment in the
+location of your choice via `JUNEST_HOME` environment variable
+(it must contain an absolute path) which by
+default is `~/.junest`:
+
+```sh
+junest setup
+```
+
+The script will download the image from the repository and will place it to the default directory `~/.junest`.
+
+Access to environment
+---------------------
+
+JuNest uses the Linux namespaces (aka `ns`) as the default backend program. To access via `ns` just type:
+
+```sh
+junest
+```
+
+You can use the command `sudo` to acquire fakeroot privileges and
+install/remove packages.
+
+Alternatively, you can access fakeroot privileges without using `sudo` all the
+time with the `-f` (or `--fakeroot`) option:
+
+```sh
+junest -f
+```
+
+Another execution mode is via [Proot](https://wiki.archlinux.org/index.php/Proot):
+
+```sh
+junest proot [-f]
+```
+
+There are multiple backend programs, each with its own pros/cons.
+To know more about the JuNest execution modes depending on the backend program
+used, see the [Usage](#usage) section below.
+
+Run JuNest installed programs directly from host OS
+---------------------------------------
+
+Programs installed within JuNest can be accessible directly from host machine
+without entering into a JuNest session
+(namely, no need to call `junest` command first).
+For instance, supposing the host OS is an Ubuntu distro you can directly
+run `pacman` by simply updating the `PATH` variable:
+
+```sh
+export PATH="$PATH:~/.junest/usr/bin_wrappers"
+sudoj pacman -S htop
+htop
+```
+
+By default the wrappers use `ns` mode. To use the `ns --fakeroot` you can use the convenient command helper `sudoj`.
+For more control on backend modes you can use the `JUNEST_ARGS` environment variable too.
+For instance, if you want to run `iftop` with real root privileges:
+
+```
+sudoj pacman -S iftop
+sudo JUNEST_ARGS="groot" iftop
+```
+
+Bin wrappers can be always recreated (e.g. in case for some reasons they get
+corrupted) with:
+
+```
+junest create-bin-wrappers -f
+```
+
+Bin wrappers are automatically generated each time they get installed inside JuNest.
+This only works for executables located in `/usr/bin` path.
+For executables in other locations (say `/usr/mybinpath`) you can only create
+wrappers manually by executing the command:
+
+```
+junest create-bin-wrappers --bin-path /usr/mybinpath
+```
+
+Obviously, to get access to the corresponding bin wrappers you will need to
+update your `PATH` variable accordingly:
+
+```
+export PATH="$PATH:~/.junest/usr/mybinpath_wrappers"
+```
+
+Install packages from AUR
+-------------------------
+
+In `ns` mode, you can easily install package from [AUR](https://aur.archlinux.org/) repository
+using the already available [`yay`](https://aur.archlinux.org/packages/yay/)
+command. In `proot` mode, JuNest does no longer support the building of AUR packages.
+
+**Remember** that in order to build packages from AUR, `base-devel` package group is required
+first:
+
+```sh
+pacman -S base-devel
+```
+
+JuNest uses a modified version of `sudo` provided by `junest/sudo-fake`. And the original `core/sudo`
+package will be ignored **(and must not be installed)** during the installation of `base-devel`.
+
+Have fun!
+---------
+
+If you are new on Arch Linux and you are not familiar with `pacman` package manager
+visit the [pacman rosetta page](https://wiki.archlinux.org/index.php/Pacman_Rosetta).
+
+Usage
+=====
+There are three different ways you can run JuNest depending on the backend program you decide to use.
+
+Linux namespaces based
+----------------------
+The [Linux namespaces](http://man7.org/linux/man-pages/man7/namespaces.7.html)
+represents the default backend program for JuNest.
+The requirements for having Linux namespaces working are:
+
+1. Kernel starting from Linux 3.8 allows unprivileged processes to create
+user and mount namespaces.
+1. The Linux kernel distro must have the user namespace enabled.
+
+In the last years, the majority of GNU/Linux distros have the user namespace
+enabled by default. This means that you do not need to have root privileges to
+access to the JuNest environment via this method.
+This
+[wiki](https://github.com/fsquillace/junest/wiki/Linux-distros-with-user-namespace-enabled-by-default)
+provides the state of the user namespace on several GNU/Linux distros.
+
+In order to run JuNest via Linux namespaces:
+
+- As normal user - Allow to make basic operations or install/remove packages
+with `sudo` command: `junest ns` or `junest`
+- As fakeroot - Allow to install/remove packages: `junest ns -f` or `junest -f`
+
+This mode is based on the fantastic
+[`bubblewrap`](https://github.com/containers/bubblewrap) command.
+
+PRoot based
+-----------
+[Proot](https://wiki.archlinux.org/index.php/Proot) represents a portable
+solution which allows unprivileged users to execute programs inside a sandbox
+and works well in most of GNU/Linux distros available.
+
+In order to run JuNest via Proot:
+
+- As normal user - Allow to make basic operations: `junest proot`
+
+- As fakeroot - Allow to install/remove packages: `junest proot -f`
+
+In `proot` mode, the minimum recommended Linux kernel for the host OS is 2.6.32 on x86 (64 bit)
+and ARM architectures. It is still possible to run JuNest on lower
+2.6.x host OS kernels but errors may appear, and some applications may
+crash. For further information, read the [Troubleshooting](#troubleshooting)
+section below.
+
+Chroot based
------------
-Just clone JuJu somewhere (for example in ~/juju):
+This solution suits only for privileged users. JuNest provides the possibility
+to run the environment via `chroot` program.
+In particular, it uses a special program called `GRoot`, a small and portable
+version of
+[arch-chroot](https://wiki.archlinux.org/index.php/Chroot)
+wrapper, that allows to bind mount directories specified by the user, such as
+`/proc`, `/sys`, `/dev`, `/tmp`, `/run/user/` and `$HOME`, before
+executing any programs inside the JuNest sandbox. In case the mounting will not
+work, JuNest is even providing the possibility to run the environment directly via
+the pure `chroot` command.
- $ git clone git://github.com/fsquillace/juju ~/juju
- $ export PATH=~/juju/bin:$PATH
+In order to run JuNest via `chroot` solutions:
-JuJu can only works on GNU/Linux OS with kernel version greater or equal
-2.6.32 on 64 bit 32 bit and ARMv6 architectures.
+- As root via `GRoot` - Allow to have fully root privileges inside JuNest environment (you need to be root for executing this): `junest groot`
+
+- As root via `chroot` - Allow to have fully root privileges inside JuNest environment (you need to be root for executing this): `junest root`
+
+Execution modes comparison table
+----------------
+The following table shows the capabilities that each backend program is able to perform:
+
+| | QEMU | Root privileges required | Manage Official Packages | Manage AUR Packages | Portability | Support | User modes |
+| --- | ---- | ------------------------ | ------------------------ | ------------------- | ----------- | ------- | ---------- |
+| **Linux Namespaces** | NO | NO | YES | YES | Poor | YES | Normal user and `fakeroot` |
+| **Proot** | YES | NO | YES | NO | YES | YES | Normal user and `fakeroot` |
+| **Chroot** | NO | YES | YES | YES | YES | YES | `root` only |
Advanced usage
---------------
-You can build a new JuJu image from scratch by running the following command:
+==============
+## Build image ##
+You can build a new JuNest image from scratch by running the following command:
- # juju -b
+```sh
+junest build [-n]
+```
-In this way the script will create a directory containing all the essentials
-files in order to make JuJu working properly (such as pacman, yaourt, arch-chroot and proot).
-Remember that the script to build the image must run in an ArchLinux OS with
-arch-install-scripts, package-query, git and the base-devel packages installed.
-To change the build directory just use the *JUJU_TMPDIR* (by default /tmp).
+The script will create a directory containing all the essentials
+files in order to make JuNest working properly (such as `pacman` and `proot`).
+The option `-n` will skip the final validation tests if they are not needed.
+Remember that the script to build the image must run in an Arch Linux OS with
+arch-install-scripts and the base-devel packages installed.
+To change the build directory just use the `JUNEST_TEMPDIR` (by default /tmp).
-After creating the image juju-x86\_64.tar.gz you can install it by running:
+After creating the image `junest-x86_64.tar.gz` you can install it by running:
- # juju -i juju-x86_64.tar.gz
+```sh
+junest setup -i junest-x86_64.tar.gz
+```
-Dependencies
-------------
-JuJu comes with a very short list of dependencies in order to be installed in most
-of GNU/Linux distributions. The dependencies needed in the host OS are:
-- bash
-- wget or curl
-- tar
-- mkdir
-- linux kernel 2.6.32+
+For more details, you can also take a look at
+[junest-builder](https://github.com/fsquillace/junest-builder)
+that contains the script and systemd service used for the automatic building
+of the JuNest image.
+
+Related wiki page:
+
+- [How to build a JuNest image using QEMU](https://github.com/fsquillace/junest/wiki/How-to-build-a-JuNest-image-using-QEMU)
+
+## Run JuNest using a different architecture via QEMU ##
+The following command will download the ARM JuNest image and will run QEMU in
+case the host OS runs on `x86_64` architecture:
+
+```sh
+$> export JUNEST_HOME=~/.junest-arm
+$> junest setup -a arm
+$> junest proot -- uname -m
+armv7l
+```
+
+## Bind directories ##
+To bind a host directory to a guest location:
+
+```sh
+junest -b "--bind /home/user/mydata /mnt/mydata"
+```
+
+Or using proot arguments:
+
+```sh
+junest proot -b "-b /mnt/mydata:/home/user/mydata"
+```
+
+The option `-b` to provide options to the backend program will work with PRoot, Namespace and GRoot backend programs.
+Check out the backend program options by passing `--help` option:
+
+```sh
+junest [u|g|p] -b "--help"
+```
+
+## Systemd integration ##
+Although JuNest has not been designed to be a complete container, it is even possible to
+virtualize the process tree thanks to the [systemd container](https://wiki.archlinux.org/index.php/Systemd-nspawn).
+The JuNest containter allows to run services inside the container that can be
+visible from the host OS through the network.
+The drawbacks of this are that the host OS must use systemd as a service manager,
+and the container can only be executed using root privileges.
+
+To boot a JuNest container:
+
+```sh
+sudo systemd-nspawn -bD ~/.junest
+```
+
+Related wiki page:
+
+- [How to run junest as a container](https://github.com/fsquillace/junest/wiki/How-to-run-JuNest-as-a-container)
+- [How to run services using Systemd](https://github.com/fsquillace/junest/wiki/How-to-run-services-using-Systemd)
+
+Internals
+=========
+## Automatic fallback for all the dependent host OS executables ##
+JuNest attempts first to run the executables in the host OS located in different
+positions (`/usr/bin`, `/bin`, `/usr/sbin` and `/sbin`).
+As a fallback it tries to run the same executable if it is available in the JuNest
+environment.
+
+## Automatic building of the JuNest images ##
+There is a periodic automation build of the JuNest images for `x86_64` arch
+only.
+The JuNest image for `arm` architecture may not be always up to date because
+the build is performed manually.
+
+## Static QEMU binaries ##
+There are static QEMU binaries included in JuNest image that allows to run JuNest
+in a different architecture from the host system. They are located in `/opt/qemu`
+directory.
Troubleshooting
----------------
+===============
-###Cannot use AUR repository###
-- **Q**: Why do I get the following error when I try to install a package with yaourt?
-```
-Cannot find the gzip binary required for compressing man and info pages.
-```
-- **A**: JuJu comes with a very basic number of packages.
-In order to install packages using yaourt you may need to install the package group *base-devel*
-that contains all the essential packages for compiling source code (such as gcc, make, patch, etc):
+For Arch Linux related FAQs take a look at the [General troubleshooting page](https://wiki.archlinux.org/index.php/General_troubleshooting).
-```
- pacman -S base-devel
-```
+## Cannot use AUR repository ##
-###Kernel too old###
-- **Q**: Why do I get the error: "FATAL: kernel too old"?
-- **A**: This is because the executable from the precompiled package cannot
-always run if the kernel is old.
-In order to check if the executable can be compatible with the kernel of
-the host OS just use file command, for instance:
+> **Q**: Why do I get the following error when I try to install a package?
-```
- file ~/.juju/usr/bin/bash
+ Cannot find the gzip binary required for compressing man and info pages.
+
+> **A**: JuNest comes with a very basic number of packages.
+> In order to install AUR packages you need to install the package group `base-devel` first
+> that contains all the essential packages for compiling from source code (such as gcc, make, patch, etc):
+
+ #> pacman -S base-devel
+
+> Remember to not install `core/sudo` as it conflicts with `junest/sudo-fake` package.
+
+## Can't set user and group as root
+
+> **Q**: In ns mode when installing package I get the following error:
+
+ warning: warning given when extracting /usr/file... (Can't set user=0/group=0 for
+ /usr/file...)
+
+> **A**: This is because as fakeroot is not possible to set the owner/group of
+> files as root. The package will still be installed correctly even though this
+> message is showed.
+
+## Could not change the root directory in pacman
+
+## No servers configured for repository ##
+
+> **Q**: Why I cannot install packages?
+
+ #> pacman -S lsof
+ Packages (1): lsof-4.88-2
+
+ Total Download Size: 0.09 MiB
+ Total Installed Size: 0.21 MiB
+
+ error: no servers configured for repository: core
+ error: no servers configured for repository: community
+ error: failed to commit transaction (no servers configured for repository)
+ Errors occurred, no packages were upgraded.
+
+> **A**: You need simply to update the mirrorlist file according to your location:
+
+ # Uncomment the repository line according to your location
+ #> nano /etc/pacman.d/mirrorlist
+ #> pacman -Syy
+
+## Locate the package for a given file ##
+
+> **Q**: How do I find which package a certain file belongs to?
+
+> **A**: JuNest is a really small distro, therefore you frequently need to find
+> the package name for a certain file. `pkgfile` is an extremely useful package
+> that allows you to detect the package of a given file.
+> For instance, if you want to find the package name for the command `getopt`:
+
+ #> pacman -S pkgfile
+ #> pkgfile --update
+ $> pkgfile getop
+ core/util-linux
+
+> Alternatively, you can use directly `pacman` command only. Take a look [here](https://wiki.archlinux.org/index.php/General_troubleshooting#Message:_%22error_while_loading_shared_libraries%22).
+
+## Kernel too old ##
+
+> **Q**: Why do I get the error: "FATAL: kernel too old"?
+
+> **A**: This is because the binaries from the precompiled package are
+> compiled for Linux kernel 2.6.32. When JuNest is started without further
+> options, it tries to run a shell from the JuNest chroot. The system sees that
+> the host OS kernel is too old and refuses to start the shell.
+
+> The solution is to present a higher "fake" kernel version to the JuNest
+> chroot. PRoot offers the *-k* option for this, and JuNest passes this option
+> on to PRoot when *-p* is prepended. For example, to fake a kernel version of
+> 3.10, issue the following command:
+
+ $> junest proot -b "-k 3.10"
+
+> As Arch Linux ships binaries for kernel version 2.6.32, the above error is
+> not unique to the precompiled package from JuNest. It will also appear when
+> trying to run binaries that were later installed in the JuNest chroot with
+> the `pacman` command.
+
+> In order to check if an executable inside JuNest chroot is compatible with
+> the kernel of the host OS just use the `file` command, for instance:
+
+ $> file ~/.junest/usr/bin/bash
ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked
(uses shared libs), for GNU/Linux 2.6.32,
BuildID[sha1]=ec37e49e7188ff4030052783e61b859113e18ca6, stripped
-```
-From the output you can see what is the minimum recommended Linux kernel version.
+> The output shows the minimum recommended Linux kernel version.
-###SUID permissions###
-- **Q**: Why I do not have permissions for ping?
-```
- ping www.google.com
+## Kernel doesn't support private futexes ##
+
+> **Q**: Why do I get the warning: "kompat: this kernel doesn't support private
+> futexes and PRoot can't emulate them."?
+
+> **A**: This happens on older host OS kernels when the trick of showing a fake
+> kernel version to the JuNest chroot is applied (see above:
+> [Kernel too old](#kernel-too-old)).
+
+> The consequence of showing a fake kernel version to the JuNest chroot is that
+> in the background, PRoot needs to translate requests from applications in the
+> chroot to the old kernel of the host OS. Some of the newer kernel
+> functionality can be emulated, but private futexes cannot be translated.
+
+> Private Futexes were introduced in Linux kernel 2.6.22. Therefore, the above
+> problem likely appears on old Linux systems, for example RHEL5 systems, which
+> are based on Linux kernel 2.6.18. Many of the core tools like `which`, `man`,
+> or `vim` run without problems while others, especially XOrg-based programs,
+> are more likely to show the warning. These are also more likely to crash
+> unexpectedly.
+
+> Currently, there is no (easy) workaround for this. In order to be fully
+> compatible with kernels below 2.6.22, both the precompiled package from
+> JuNest and all software that is installed later needs to be compiled for this
+> kernel. Most likely this can only be achieved by building the needed software
+> packages from source, which kind of contradicts JuNest's distro-in-a-distro
+> philosophy.
+
+## SUID permissions ##
+> **Q**: Why I do not have permissions for ping?
+
+ $> ping www.google.com
ping: icmp open socket: Operation not permitted
-```
-- **A**: The ping command uses *suid* permissions that allow to execute the command using
-root privileges. The fakeroot mode is not able to execute a command set with suid,
-and you may need to use root privileges. There are other few commands that
-have *suid* permission, you can list the commands from your JuJu environment
-with the following command:
-```
- find /usr/bin -perm +4000
-```
+> **A**: The ping command uses *suid* permissions that allow to execute the command using
+> root privileges. The fakeroot mode is not able to execute a command set with suid,
+> and you may need to use root privileges. There are other few commands that
+> have *suid* permission, you can list the commands from your JuNest environment
+> with the following command:
-###No characters are visible on a graphic application###
-- **Q**: Why I do not see any characters in the application I have installed?
+ $> find /usr/bin -perm /4000
-- **A**: This is probably because there are no
-[https://wiki.archlinux.org/index.php/Font_Configuration](fonts) installed in
-the system.
+## No characters are visible on a graphic application ##
-To quick fix this, you can just install a fonts package:
-```
- pacman -S gnu-free-fonts
-```
+> **Q**: Why I do not see any characters in the application I have installed?
-###Missing permissions on removing a package###
-- **Q**: Why I cannot remove the package I have installed?
-```
- pacman -Rsn lsof
- checking dependencies...
+> **A**: This is probably because there are no
+> [fonts](https://wiki.archlinux.org/index.php/Font_Configuration) installed in
+> the system.
- Packages (1): lsof-4.88-1
+> To quick fix this, you can just install a fonts package:
- Total Removed Size: 0.21 MiB
+ #> pacman -S gnu-free-fonts
- error: cannot remove /usr/share/licenses/lsof/LICENSE (Permission denied)
- error: could not remove database entry lsof-4.88-1
-```
+## Differences between filesystem and package ownership ##
-- **A**: This is probably because you have installed the package with root
-permissions. Since JuJu gives the possibility to install packages
-either as root or as normal user you need to remember that and remove
-the package with the right user!
+> **Q**: Why do I get warning when I install a package using root privileges?
-License
--------
-Copyright (c) 2012-2014
+ #> pacman -S systat
+ ...
+ warning: directory ownership differs on /usr/
+ filesystem: 1000:100 package: 0:0
+ ...
-This program is free software; you can redistribute it and/or modify it
-under the terms of the GNU Library General Public License as published
-by the Free Software Foundation; either version 2, or (at your option)
-any later version.
+> **A**: In these cases the package installation went smoothly anyway.
+> This should happen every time you install package with root privileges
+> since JuNest will try to preserve the JuNest environment by assigning ownership
+> of the files to the real user.
-This program is distributed in the hope that it will be useful,
-but WITHOUT ANY WARRANTY; without even the implied warranty of
-MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-GNU General Public License for more details.
+## Unprivileged user namespace disable at kernel compile time or kernel too old ##
-You should have received a copy of the GNU General Public License
-along with this program. If not, see .
+> **Q**: Why do I get this warning when I run JuNest via Linux namespaces?
-## Author
-Filippo Squillace
+ $> junest ns
+ Unprivileged user namespace is disabled at kernel compile time or kernel too old (<3.8). Proceeding anyway...
-## WWW
-https://github.com/fsquillace/juju
+> **A**: This means that JuNest detected that the host OS either
+> does not have a newer kernel version or the unprivileged user namespace
+> is not enabled at kernel compile time.
+> JuNest does not stop the execution of the program but it attempts to run it
+> anyway. Try to use Proot as backend program in case is not possible to invoke namespaces.
+
+## Unprivileged user namespace disabled
+
+> **Q**: Why do I get this warning when I run JuNest via Linux namespaces?
+
+ $> junest ns
+ Unprivileged user namespace disabled. Root permissions are required to enable it: sudo sysctl kernel.unprivileged_userns_clone=1
+
+> **A**: This means that JuNest detected that the host OS either
+> does not have a newer Linux version or the user namespace is not enabled.
+> JuNest does not stop the execution of the program but it attempts to run it
+> anyway. If you have root permissions try to enable it, otherwise try to use
+> Proot as backend program.
+
+More documentation
+==================
+There are additional tutorials in the
+[JuNest wiki page](https://github.com/fsquillace/junest/wiki).
+
+Contributing
+============
+Contributions are welcome! You could help improving JuNest in the following ways:
+
+- [Reporting Bugs](CONTRIBUTING.md#reporting-bugs)
+- [Suggesting Enhancements](CONTRIBUTING.md#suggesting-enhancements)
+- [Writing Code](CONTRIBUTING.md#your-first-code-contribution)
+
+Donating
+========
+To sustain the project please consider funding by donations through
+the [GitHub Sponsors page](https://github.com/sponsors/fsquillace/).
+
+Authors
+=======
+JuNest was originally created in late 2014 by [Filippo Squillace (feel.sqoox@gmail.com)](https://github.com/fsquillace).
+
+Here is a list of [**really appreciated contributors**](https://github.com/fsquillace/junest/graphs/contributors)!
+
+[](https://sourcerer.io/fame/fsquillace/fsquillace/junest/links/0)[](https://sourcerer.io/fame/fsquillace/fsquillace/junest/links/1)[](https://sourcerer.io/fame/fsquillace/fsquillace/junest/links/2)[](https://sourcerer.io/fame/fsquillace/fsquillace/junest/links/3)[](https://sourcerer.io/fame/fsquillace/fsquillace/junest/links/4)[](https://sourcerer.io/fame/fsquillace/fsquillace/junest/links/5)[](https://sourcerer.io/fame/fsquillace/fsquillace/junest/links/6)[](https://sourcerer.io/fame/fsquillace/fsquillace/junest/links/7)
diff --git a/VERSION b/VERSION
new file mode 100644
index 0000000..ef13716
--- /dev/null
+++ b/VERSION
@@ -0,0 +1 @@
+7.4.10
diff --git a/bin/juju b/bin/juju
deleted file mode 100755
index 6f5e68c..0000000
--- a/bin/juju
+++ /dev/null
@@ -1,166 +0,0 @@
-#!/usr/bin/env bash
-#
-# This file is part of JuJu: The portable GNU/Linux distribution
-#
-# Copyright (c) 2012-2014 Filippo Squillace
-#
-# This program is free software; you can redistribute it and/or modify it
-# under the terms of the GNU Library General Public License as published
-# by the Free Software Foundation; either version 2, or (at your option)
-# any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program. If not, see .
-#
-
-NAME='juju'
-VERSION='1.0'
-
-source "$(dirname $0)/../lib/core.sh"
-
-###################################
-### General functions ###
-###################################
-
-usage() {
- echo -e "JuJu: The portable GNU/Linux distribution"
- echo -e "Usage: $NAME [options]"
- echo -e "Options:"
- echo -e "-i, --setup-from-file Setup the JuJu image in ${JUJU_HOME}"
- echo -e "-f, --fakeroot Run JuJu with fakeroot privileges"
- echo -e "-r, --root Run JuJu with root privileges"
- echo -e "-b, --build-image Build a JuJu image (must run in ArchLinux)"
- echo -e "-d, --delete Delete JuJu from ${JUJU_HOME}"
- echo -e "-h, --help Show this help message"
- echo -e "-v, --version Show the $NAME version"
-}
-
-version() {
- echo -e "JuJu ($VERSION): The portable GNU/Linux distribution"
- echo -e "Copyright (c) 2012-2014 Filippo Squillace "
- echo -e "Homepage: http://github.com/fsquillace/juju"
-}
-
-check_cli(){
- if $OPT_BUILD_IMAGE
- then
- if $OPT_DELETE || $OPT_HELP || $OPT_VERSION || $OPT_SETUP_FROM_FILE || \
- $OPT_FAKEROOT || $OPT_ROOT
- then
- die "The build image option must be used exclusively"
- fi
- fi
- if $OPT_DELETE
- then
- if $OPT_BUILD_IMAGE || $OPT_HELP || $OPT_VERSION || $OPT_SETUP_FROM_FILE || \
- $OPT_FAKEROOT || $OPT_ROOT
- then
- die "The JuJu delete option must be used exclusively"
- fi
- fi
- if $OPT_HELP
- then
- if $OPT_BUILD_IMAGE || $OPT_DELETE || $OPT_VERSION || $OPT_SETUP_FROM_FILE || \
- $OPT_FAKEROOT || $OPT_ROOT
- then
- die "The JuJu help option must be used exclusively"
- fi
- fi
- if $OPT_VERSION
- then
- if $OPT_BUILD_IMAGE || $OPT_DELETE || $OPT_HELP || $OPT_SETUP_FROM_FILE || \
- $OPT_FAKEROOT || $OPT_ROOT
- then
- die "The JuJu version option must be used exclusively"
- fi
- fi
-
- if $OPT_FAKEROOT && $OPT_ROOT
- then
- die "You must access to JuJu with either fakeroot or root permissions"
- fi
-
- [ "$ARGS" != "" ] && die "No arguments are needed. For the CLI syntax run: $NAME --help"
-
- return 0
-}
-
-
-###################################
-### MAIN PROGRAM ###
-###################################
-
-TEMP=`getopt -o drfbi:hv --long delete,root,fakeroot,build-image,setup-from-file:,help,version -n 'juju' -- "$@"`
-
-if [ $? != 0 ] ; then error "Error on parsing the command line. Try juju -h." ; exit ; fi
-
-# Note the quotes around `$TEMP': they are essential!
-eval set -- "$TEMP"
-
-OPT_SETUP_FROM_FILE=false
-IMAGE_FILE=""
-OPT_FAKEROOT=false
-OPT_ROOT=false
-OPT_BUILD_IMAGE=false
-OPT_DELETE=false
-OPT_HELP=false
-OPT_VERSION=false
-while true ; do
- case "$1" in
- -i|--setup-from-file) OPT_SETUP_FROM_FILE=true ; shift ; IMAGE_FILE=$1 ; shift ;;
- -f|--fakeroot) OPT_FAKEROOT=true ; shift ;;
- -r|--root) OPT_ROOT=true ; shift ;;
- -b|--build-image) OPT_BUILD_IMAGE=true ; shift ;;
- -d|--delete) OPT_DELETE=true ; shift ;;
- -h|--help) OPT_HELP=true ; shift ;;
- -v|--version) OPT_VERSION=true ; shift ;;
- --) shift ; break ;;
- *) error "Internal error!" ; exit 1 ;;
- esac
-done
-
-ARGS=()
-for arg do
- ARGS+=($arg)
-done
-
-check_cli || exit 1
-
-################ DEFINE ACTION ########################
-
-$OPT_HELP && usage && exit
-$OPT_VERSION && version && exit
-
-if $OPT_BUILD_IMAGE; then
- build_image_juju
- exit
-elif $OPT_DELETE; then
- delete_juju
- exit
-fi
-
-if ! is_juju_installed
-then
- if $OPT_SETUP_FROM_FILE; then
- setup_from_file_juju $IMAGE_FILE
- else
- setup_juju
- fi
-elif $OPT_SETUP_FROM_FILE; then
- die "Error: The image cannot be installed since $JUJU_HOME is not empty."
-fi
-
-if $OPT_FAKEROOT; then
- run_juju_as_fakeroot
-elif $OPT_ROOT; then
- run_juju_as_root
-else
- run_juju_as_user
-fi
-
-# vim: set ts=4 sw=4 noet:
diff --git a/bin/junest b/bin/junest
new file mode 100755
index 0000000..4084fe0
--- /dev/null
+++ b/bin/junest
@@ -0,0 +1,322 @@
+#!/usr/bin/env bash
+# shellcheck disable=SC1091
+#
+# This file is part of JuNest (https://github.com/fsquillace/junest).
+#
+
+set -e
+
+# JUNEST_BASE can be overridden for testing purposes.
+# There is no need for doing it for normal usage.
+JUNEST_BASE="${JUNEST_BASE:-$(readlink -f "$(dirname "$(readlink -f "$0")")"/..)}"
+
+source "${JUNEST_BASE}/lib/utils/utils.sh"
+source "${JUNEST_BASE}/lib/core/common.sh"
+
+source "${JUNEST_BASE}/lib/core/build.sh"
+source "${JUNEST_BASE}/lib/core/setup.sh"
+source "${JUNEST_BASE}/lib/core/chroot.sh"
+source "${JUNEST_BASE}/lib/core/namespace.sh"
+source "${JUNEST_BASE}/lib/core/proot.sh"
+source "${JUNEST_BASE}/lib/core/wrappers.sh"
+
+
+###################################
+### General functions ###
+###################################
+usage() {
+ echo -e "$NAME (v$(cat "$JUNEST_BASE"/VERSION)): $DESCRIPTION"
+ echo
+ echo -e "Usage: $CMD [action] [options] [--] [command]"
+ echo
+ echo -e "General:"
+ echo -e "-h, --help Show this help message"
+ echo -e "-V, --version Show the $NAME version"
+ echo
+ echo -e "Actions and options:"
+ echo -e " s[etup] Setup $NAME in ${JUNEST_HOME} either from repo or from file"
+ echo -e " -i, --from-file Setup the $NAME image in ${JUNEST_HOME}"
+ echo -e " -a, --arch $NAME architecture to download (x86_64, arm)"
+ echo -e " Defaults to the host architecture ($ARCH)"
+ echo -e " -d, --delete Delete $NAME from ${JUNEST_HOME}"
+ echo
+ echo -e " n[s] Access via Linux Namespaces using BubbleWrap (Default action)"
+ echo -e " -f, --fakeroot Run $NAME with fakeroot privileges"
+ echo -e " --backend-command Bwrap command to use"
+ echo -e " -b, --backend-args Arguments for bwrap backend program"
+ echo -e " ($CMD ns -b \"--help\" to check out the bwrap options)"
+ echo -e " -n, --no-copy-files Do not copy common etc files into $NAME environment"
+ echo
+ echo -e " p[root] Access via PRoot"
+ echo -e " -f, --fakeroot Run $NAME with fakeroot privileges"
+ echo -e " --backend-command PRoot command to use"
+ echo -e " -b, --backend-args Arguments for PRoot backend program"
+ echo -e " ($CMD proot -b \"--help\" to check out the PRoot options)"
+ echo -e " -n, --no-copy-files Do not copy common etc files into $NAME environment"
+ echo
+ echo -e " g[root] Access with root privileges via GRoot"
+ echo -e " --backend-command GRoot command to use"
+ echo -e " -b, --backend-args Arguments for GRoot backend program"
+ echo -e " ($CMD groot -b \"--help\" to check out the GRoot options)"
+ echo -e " -n, --no-copy-files Do not copy common etc files into $NAME environment"
+ echo
+ echo -e " r[oot] Access with root privileges via classic chroot"
+ echo -e " --backend-command Chroot command to use"
+ echo -e " -b, --backend-args Arguments for chroot backend program"
+ echo -e " ($CMD root -b \"--help\" to check out the chroot options)"
+ echo -e " -n, --no-copy-files Do not copy common etc files into $NAME environment"
+ echo
+ echo -e " b[uild] Build a $NAME image (must run in ArchLinux)"
+ echo -e " -n, --disable-check Disable the $NAME image check"
+ echo
+ echo -e " create-bin-wrappers Create a bin wrappers directory according to --bin-path option"
+ echo -e " Default path is $JUNEST_HOME/usr/bin_wrappers"
+ echo -e " -f, --force Create the wrapper files even if they already exist"
+ echo -e " -p, --bin-path The source directory where executable are located in JuNest"
+ echo -e " Default value is: /usr/bin"
+ echo
+}
+
+version() {
+ echo -e "$NAME $(cat "$JUNEST_BASE"/VERSION)"
+}
+
+function parse_arguments(){
+ # Actions
+ ACT_SETUP=false
+ ACT_BUILD=false
+ ACT_CREATE_WRAPPERS=false
+ ACT_NAMESPACE=false
+ ACT_PROOT=false
+ ACT_GROOT=false
+ ACT_ROOT=false
+ ACT_HELP=false
+ ACT_VERSION=false
+
+ case "$1" in
+ s|setup) ACT_SETUP=true ; shift ;;
+ b|build) ACT_BUILD=true ; shift ;;
+ create-bin-wrappers) ACT_CREATE_WRAPPERS=true ; shift ;;
+ n|ns) ACT_NAMESPACE=true ; shift ;;
+ p|proot) ACT_PROOT=true ; shift ;;
+ g|groot) ACT_GROOT=true ; shift ;;
+ r|root) ACT_ROOT=true ; shift ;;
+ -h|--help) ACT_HELP=true ; shift ;;
+ -V|--version) ACT_VERSION=true ; shift ;;
+ *) ACT_NAMESPACE=true ;;
+ esac
+
+ if $ACT_SETUP
+ then
+ _parse_setup_opts "$@"
+ elif $ACT_BUILD
+ then
+ _parse_build_opts "$@"
+ elif $ACT_CREATE_WRAPPERS
+ then
+ _parse_create_wrappers_opts "$@"
+ elif $ACT_NAMESPACE
+ then
+ _parse_ns_opts "$@"
+ elif $ACT_PROOT
+ then
+ _parse_proot_opts "$@"
+ elif $ACT_GROOT
+ then
+ _parse_root_opts "$@"
+ elif $ACT_ROOT
+ then
+ _parse_root_opts "$@"
+ fi
+}
+
+function _parse_root_opts() {
+ # Options:
+ BACKEND_ARGS=""
+ OPT_NO_COPY_FILES=false
+ BACKEND_COMMAND=""
+
+ while [[ -n "$1" ]]
+ do
+ case "$1" in
+ -b|--backend-args) shift ; BACKEND_ARGS=$1; shift ;;
+ -n|--no-copy-files) OPT_NO_COPY_FILES=true ; shift ;;
+ --backend-command) shift; BACKEND_COMMAND="$1"; shift ;;
+ --) shift ; break ;;
+ -*) die "Invalid option $1" ;;
+ *) break ;;
+ esac
+ done
+
+ ARGS=()
+ for arg in "$@"
+ do
+ ARGS+=("$arg")
+ done
+}
+
+function _parse_ns_opts() {
+ # Options:
+ OPT_FAKEROOT=false
+ BACKEND_ARGS=""
+ OPT_NO_COPY_FILES=false
+ BACKEND_COMMAND=""
+
+ while [[ -n "$1" ]]
+ do
+ case "$1" in
+ -f|--fakeroot) OPT_FAKEROOT=true ; shift ;;
+ -b|--backend-args) shift ; BACKEND_ARGS=$1; shift ;;
+ -n|--no-copy-files) OPT_NO_COPY_FILES=true ; shift ;;
+ --backend-command) shift; BACKEND_COMMAND="$1"; shift ;;
+ --) shift ; break ;;
+ -*) die "Invalid option $1" ;;
+ *) break ;;
+ esac
+ done
+
+ ARGS=()
+ for arg in "$@"
+ do
+ ARGS+=("$arg")
+ done
+}
+
+function _parse_proot_opts() {
+ # Options:
+ OPT_FAKEROOT=false
+ BACKEND_ARGS=""
+ OPT_NO_COPY_FILES=false
+ BACKEND_COMMAND=""
+
+ while [[ -n "$1" ]]
+ do
+ case "$1" in
+ -f|--fakeroot) OPT_FAKEROOT=true ; shift ;;
+ -b|--backend-args) shift ; BACKEND_ARGS=$1; shift ;;
+ -n|--no-copy-files) OPT_NO_COPY_FILES=true ; shift ;;
+ --backend-command) shift; BACKEND_COMMAND="$1"; shift ;;
+ --) shift ; break ;;
+ -*) die "Invalid option $1" ;;
+ *) break ;;
+ esac
+ done
+
+ ARGS=("$@")
+}
+
+function _parse_build_opts() {
+ OPT_DISABLE_CHECK=false
+ while [[ -n "$1" ]]
+ do
+ case "$1" in
+ -n|--disable-check) OPT_DISABLE_CHECK=true ; shift ;;
+ *) die "Invalid option $1" ;;
+ esac
+ done
+}
+
+function _parse_create_wrappers_opts() {
+ OPT_FORCE=false
+ OPT_BIN_PATH=""
+ while [[ -n "$1" ]]
+ do
+ case "$1" in
+ -f|--force) OPT_FORCE=true ; shift ;;
+ -p|--bin-path) shift ; OPT_BIN_PATH="$1" ; shift ;;
+ *) die "Invalid option $1" ;;
+ esac
+ done
+}
+
+function _parse_setup_opts() {
+ OPT_FROM_FILE=false
+ IMAGE_FILE=""
+ ARCH_ARG=""
+ OPT_DELETE=false
+ while [[ -n "$1" ]]
+ do
+ case "$1" in
+ -i|--from-file) OPT_FROM_FILE=true ; shift ; IMAGE_FILE=$1 ; shift ;;
+ -a|--arch) shift ; ARCH_ARG=$1; shift ;;
+ -d|--delete) OPT_DELETE=true ; shift ;;
+ *) die "Invalid option $1" ;;
+ esac
+ done
+}
+
+function execute_operation() {
+ $ACT_HELP && usage && return
+ $ACT_VERSION && version && return
+
+ if $ACT_BUILD; then
+ # shellcheck disable=SC2086
+ build_image_env $OPT_DISABLE_CHECK
+ return
+ fi
+
+ if $ACT_SETUP; then
+ if $OPT_DELETE; then
+ delete_env
+ else
+ if is_env_installed
+ then
+ die "Error: The image cannot be installed since $JUNEST_HOME is not empty."
+ fi
+
+ if $OPT_FROM_FILE; then
+ setup_env_from_file "$IMAGE_FILE"
+ else
+ setup_env "$ARCH_ARG"
+ fi
+ create_wrappers
+ fi
+
+ return
+ fi
+
+
+ if ! is_env_installed
+ then
+ die "Error: The image is still not installed in $JUNEST_HOME. Run this first: $CMD setup"
+ fi
+
+ if $ACT_CREATE_WRAPPERS; then
+ # shellcheck disable=SC2086
+ create_wrappers $OPT_FORCE "$OPT_BIN_PATH"
+ exit
+ fi
+
+ local run_env
+ if $ACT_NAMESPACE; then
+ if $OPT_FAKEROOT; then
+ run_env=run_env_as_bwrap_fakeroot
+ else
+ run_env=run_env_as_bwrap_user
+ fi
+ elif $ACT_PROOT; then
+ if $OPT_FAKEROOT; then
+ run_env=run_env_as_proot_fakeroot
+ else
+ run_env=run_env_as_proot_user
+ fi
+ elif $ACT_GROOT; then
+ run_env=run_env_as_groot
+ elif $ACT_ROOT; then
+ run_env=run_env_as_chroot
+ fi
+
+ # Call create_wrappers in case new bin files have been created
+ # shellcheck disable=SC2064
+ trap "PATH=$PATH create_wrappers" EXIT QUIT TERM
+ # shellcheck disable=SC2086
+ $run_env "$BACKEND_COMMAND" "${BACKEND_ARGS}" $OPT_NO_COPY_FILES "${ARGS[@]}"
+}
+
+function main() {
+ parse_arguments "$@"
+ execute_operation
+}
+
+main "$@"
+# vim: set ts=4 sw=4 noet:
diff --git a/bin/sudoj b/bin/sudoj
new file mode 100755
index 0000000..aa43e15
--- /dev/null
+++ b/bin/sudoj
@@ -0,0 +1,8 @@
+#!/usr/bin/env bash
+#
+# This file is part of JuNest (https://github.com/fsquillace/junest).
+#
+
+export PATH="${PATH}:${JUNEST_HOME}/usr/bin_wrappers"
+
+JUNEST_ARGS="ns --fakeroot" "$@"
diff --git a/ci/build_image.sh b/ci/build_image.sh
new file mode 100755
index 0000000..f9cda95
--- /dev/null
+++ b/ci/build_image.sh
@@ -0,0 +1,15 @@
+#!/usr/bin/env bash
+
+set -ex
+
+pacman -Sy --noconfirm sudo
+
+# Create a travis user
+echo "travis ALL=(ALL) NOPASSWD: ALL" > /etc/sudoers.d/travis
+chmod 'u=r,g=r,o=' /etc/sudoers.d/travis
+groupadd --gid "2000" "travis"
+useradd --create-home --uid "2000" --gid "2000" --shell /usr/bin/false "travis"
+
+# Here do not make any validation (-n) because it will be done later on in the Ubuntu host directly
+cd /build
+runuser -u travis -- /build/bin/junest build -n
diff --git a/ci/deploy.sh b/ci/deploy.sh
new file mode 100755
index 0000000..a45d0a1
--- /dev/null
+++ b/ci/deploy.sh
@@ -0,0 +1,48 @@
+#!/usr/bin/env bash
+
+set -e
+
+IMG_PATH=$1
+
+set -ux
+
+MAX_OLD_IMAGES=5
+ENDPOINT="https://8da1bcd84e423c9b013b69fe1e8b4675.r2.cloudflarestorage.com"
+
+# ARCH can be one of: x86, x86_64, arm
+HOST_ARCH=$(uname -m)
+if [ "$HOST_ARCH" == "i686" ] || [ "$HOST_ARCH" == "i386" ]
+then
+ ARCH="x86"
+elif [ "$HOST_ARCH" == "x86_64" ]
+then
+ ARCH="x86_64"
+elif [[ $HOST_ARCH =~ .*(arm).* ]]
+then
+ ARCH="arm"
+else
+ echo "Unknown architecture ${HOST_ARCH}" >&2
+ exit 11
+fi
+
+if [[ "$TRAVIS_BRANCH" == "master" ]]
+then
+
+ export AWS_DEFAULT_REGION=auto
+ # Upload image
+ # The put is done via a temporary filename in order to prevent outage on the
+ # production file for a longer period of time.
+ img_name=$(basename "${IMG_PATH}")
+ aws s3 --endpoint-url="$ENDPOINT" cp "${IMG_PATH}" s3://junest-repo/junest/
+
+ DATE=$(date +'%Y-%m-%d-%H-%M-%S')
+ aws s3 --endpoint-url="$ENDPOINT" cp "${IMG_PATH}" "s3://junest-repo/junest/${img_name}.${DATE}"
+
+ # Cleanup old images
+ aws s3 --endpoint-url="$ENDPOINT" ls s3://junest-repo/junest/junest-${ARCH}.tar.gz. | awk '{print $4}' | head -n -${MAX_OLD_IMAGES} | xargs -I {} aws s3 --endpoint-url="$ENDPOINT" rm "s3://junest-repo/junest/{}"
+
+ # Test the newly deployed image can be downloaded correctly
+ junest setup
+ junest -- echo "Installed JuNest (\$(uname -m))"
+ yes | junest setup --delete
+fi
diff --git a/ci/install-bash.sh b/ci/install-bash.sh
new file mode 100755
index 0000000..b766123
--- /dev/null
+++ b/ci/install-bash.sh
@@ -0,0 +1,13 @@
+#!/bin/sh
+set -ex
+
+VERSION=$1
+
+cd /tmp
+wget "http://ftp.gnu.org/gnu/bash/bash-$VERSION.tar.gz"
+
+tar -zxf "bash-$VERSION.tar.gz"
+cd /tmp/bash-"$VERSION"*
+./configure
+make
+sudo make install
diff --git a/lib/checks/check.sh b/lib/checks/check.sh
new file mode 100755
index 0000000..789206e
--- /dev/null
+++ b/lib/checks/check.sh
@@ -0,0 +1,102 @@
+#!/usr/bin/env bash
+# shellcheck disable=SC1091
+#
+# This modules is used for:
+# - Running checks against the building JuNest image
+# - Integration tests on JuNest script against different execution modes (i.e. -f, -u, -r modes)
+#
+# Dependencies:
+# - None
+#
+# vim: ft=sh
+
+set -ex
+
+
+RUN_ROOT_TESTS=false
+SKIP_AUR_TESTS=false
+USE_SUDO=false
+while [[ -n "$1" ]]
+do
+ case "$1" in
+ --run-root-tests) RUN_ROOT_TESTS=true ; shift ;;
+ --skip-aur-tests) SKIP_AUR_TESTS=true ; shift ;;
+ --use-sudo) USE_SUDO=true ; shift ;;
+ *) die "Invalid option $1" ;;
+ esac
+done
+
+set -u
+
+SUDO=""
+[[ -n $USE_SUDO ]] && SUDO="sudo"
+
+JUNEST_HOME=${JUNEST_HOME:-$HOME/.junest}
+
+# JUNEST_BASE can be overridden for testing purposes.
+# There is no need for doing it for normal usage.
+JUNEST_BASE="${JUNEST_BASE:-$(readlink -f "$(dirname "$(readlink -f "$0")")"/../..)}"
+
+source "${JUNEST_BASE}/lib/utils/utils.sh"
+source "${JUNEST_BASE}/lib/core/common.sh"
+
+info "Validating JuNest located in ${JUNEST_HOME}..."
+
+info "Initial JuNest setup..."
+# The following ensures that the gpg agent gets killed (if exists)
+# otherwise it is not possible to exit from the session
+trap "[[ -e /etc/pacman.d/gnupg/S.gpg-agent ]] && gpg-connect-agent -S /etc/pacman.d/gnupg/S.gpg-agent killagent /bye" QUIT EXIT ABRT TERM INT
+
+prepare_archlinux "$SUDO"
+
+PACMAN_OPTIONS="--noconfirm --disable-download-timeout"
+# shellcheck disable=SC2086
+$SUDO pacman $PACMAN_OPTIONS -S grep coreutils
+# shellcheck disable=SC2086
+# shellcheck disable=SC2046
+$SUDO pacman $PACMAN_OPTIONS -Syu --ignore sudo base-devel
+
+info "Checking basic executables work..."
+$SUDO pacman -Qi pacman 1> /dev/null
+/usr/bin/groot --help 1> /dev/null
+
+# Test FAKEROOTDONTTRYCHOWN is set to true by default
+set +u
+if [[ -z $FAKEROOTKEY ]]
+then
+ fakeroot chown root /tmp
+else
+ chown root /tmp
+fi
+set -u
+
+repo_package1=tree
+echo "Checking ${repo_package1} package from official repo..."
+# shellcheck disable=SC2086
+$SUDO pacman $PACMAN_OPTIONS -S ${repo_package1}
+tree -L 1
+# shellcheck disable=SC2086
+$SUDO pacman $PACMAN_OPTIONS -Rsn ${repo_package1}
+
+repo_package2=iftop
+info "Checking ${repo_package2} package from official repo..."
+# shellcheck disable=SC2086
+$SUDO pacman $PACMAN_OPTIONS -S ${repo_package2}
+if $RUN_ROOT_TESTS
+then
+ # Time it out given that sometimes it gets stuck after few seconds.
+ $SUDO timeout 10 iftop -t -s 5 || true
+fi
+# shellcheck disable=SC2086
+$SUDO pacman $PACMAN_OPTIONS -Rsn ${repo_package2}
+
+if ! $SKIP_AUR_TESTS
+then
+ aur_package=tcptraceroute
+ info "Checking ${aur_package} package from AUR repo..."
+ yay --noconfirm -S ${aur_package}
+ # shellcheck disable=SC2086
+ $SUDO pacman $PACMAN_OPTIONS -Rsn ${aur_package}
+fi
+
+exit 0
diff --git a/lib/checks/check_all.sh b/lib/checks/check_all.sh
new file mode 100755
index 0000000..13e9237
--- /dev/null
+++ b/lib/checks/check_all.sh
@@ -0,0 +1,31 @@
+#!/usr/bin/env bash
+# Multiple tests against different execution modes
+
+set -ex
+
+# JUNEST_BASE can be overridden for testing purposes.
+# There is no need for doing it for normal usage.
+JUNEST_BASE="${JUNEST_BASE:-$(readlink -f "$(dirname "$(readlink -f "$0")")"/../..)}"
+
+JUNEST_SCRIPT=${JUNEST_SCRIPT:-${JUNEST_BASE}/bin/junest}
+
+CHECK_SCRIPT=${JUNEST_BASE}/lib/checks/check.sh
+
+$JUNEST_SCRIPT proot --fakeroot -- "$CHECK_SCRIPT" --skip-aur-tests
+$JUNEST_SCRIPT proot -- "$CHECK_SCRIPT" --skip-aur-tests --use-sudo
+# Test the backend command option
+$JUNEST_SCRIPT proot --backend-command "$JUNEST_HOME/usr/bin/proot-x86_64" -- exit
+$JUNEST_SCRIPT ns --fakeroot -- "$CHECK_SCRIPT" --skip-aur-tests
+$JUNEST_SCRIPT ns -- "$CHECK_SCRIPT" --use-sudo
+# Test the backend command option
+$JUNEST_SCRIPT ns --backend-command "$JUNEST_HOME/usr/bin/bwrap" -- exit
+sudo -E "$JUNEST_SCRIPT" groot -- "$CHECK_SCRIPT" --run-root-tests --skip-aur-tests
+
+# Test the wrappers work
+"$JUNEST_SCRIPT" create-bin-wrappers --force
+"$JUNEST_HOME"/usr/bin_wrappers/pacman --help
+
+"$JUNEST_SCRIPT" create-bin-wrappers --force --bin-path /usr/bin/core_perl/
+"$JUNEST_HOME"/usr/bin/core_perl_wrappers/shasum --help
+
+"${JUNEST_BASE}/bin/sudoj" pacman -Syu
diff --git a/lib/core.sh b/lib/core.sh
deleted file mode 100644
index 925696d..0000000
--- a/lib/core.sh
+++ /dev/null
@@ -1,256 +0,0 @@
-#!/usr/bin/env bash
-#
-# This file is part of JuJu: The portable GNU/Linux distribution
-#
-# Copyright (c) 2012-2014 Filippo Squillace
-#
-# This program is free software; you can redistribute it and/or modify it
-# under the terms of the GNU Library General Public License as published
-# by the Free Software Foundation; either version 2, or (at your option)
-# any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program. If not, see .
-#
-
-# References:
-# https://wiki.archlinux.org/index.php/PKGBUILD
-# https://wiki.archlinux.org/index.php/Creating_Packages
-
-set -e
-
-################################ IMPORTS #################################
-source "$(dirname ${BASH_ARGV[0]})/util.sh"
-
-################################# VARIABLES ##############################
-[ -z ${JUJU_HOME} ] && JUJU_HOME=~/.juju
-if [ -z ${JUJU_TEMPDIR} ] || [ ! -d ${JUJU_TEMPDIR} ]
-then
- JUJU_TEMPDIR=/tmp
-fi
-JUJU_REPO=https://bitbucket.org/fsquillace/juju-repo/raw/master
-ORIGIN_WD=$(pwd)
-
-# The essentials executables that MUST exist in the host OS are (wget|curl), bash, mkdir
-if command -v wget > /dev/null 2>&1
-then
- WGET="wget --no-check-certificate"
-elif command -v curl > /dev/null 2>&1
-then
- WGET="curl -J -O -k"
-else
- die "Error: Either wget or curl commands must be installed"
-fi
-TAR=tar
-
-ARCH=$(uname -m)
-[[ $ARCH =~ .*(armv6).* ]] && ARCH=${BASH_REMATCH[1]}
-
-if [ $ARCH == "i686" ]
-then
- LD_LIB="${JUJU_HOME}/lib/ld-linux.so.2"
-elif [ $ARCH == "x86_64" ]
-then
- LD_LIB="${JUJU_HOME}/lib64/ld-linux-x86-64.so.2"
-elif [ $ARCH == "armv6" ]
-then
- LD_LIB="${JUJU_HOME}/lib/ld-linux-armhf.so.3"
-else
- die "Unknown architecture ${ARCH}"
-fi
-
-if [ -z $JUJU_ENV ] || [ "$JUJU_ENV" == "0" ]
-then
- PROOT="$LD_LIB --library-path ${JUJU_HOME}/usr/lib:${JUJU_HOME}/lib ${JUJU_HOME}/usr/bin/proot"
- SH="/bin/sh --login"
-elif [ "$JUJU_ENV" == "1" ]
-then
- PROOT="$LD_LIB"
- SH="/bin/sh"
-else
- die "The variable JUJU_ENV is not properly set"
-fi
-################################# MAIN FUNCTIONS ##############################
-
-function is_juju_installed(){
- [ -d "$JUJU_HOME" ] && [ "$(ls -A $JUJU_HOME)" ] && return 0
- return 1
-}
-
-
-function cleanup_build_directory(){
-# $1: maindir (optional) - str: build directory to get rid
- local maindir=$1
- builtin cd $ORIGIN_WD
- trap - QUIT EXIT ABRT KILL TERM INT
- rm -fr "$maindir"
-}
-
-
-function prepare_build_directory(){
- trap - QUIT EXIT ABRT KILL TERM INT
- trap "rm -rf ${maindir}; die \"Error occurred when installing JuJu\"" EXIT QUIT ABRT KILL TERM INT
-}
-
-
-function _setup_juju(){
- is_juju_installed && die "Error: JuJu has been already installed in $JUJU_HOME"
- mkdir -p "${JUJU_HOME}"
- imagepath=$1
- tar -zxpf ${imagepath} -C ${JUJU_HOME}
- mkdir -p ${JUJU_HOME}/run/lock
- info "JuJu installed successfully"
-}
-
-
-function setup_juju(){
-# Setup the JuJu environment
- [ "$JUJU_ENV" == "1" ] && die "Error: The operation is not allowed inside JuJu environment"
-
- local maindir=$(TMPDIR=$JUJU_TEMPDIR mktemp -d -t juju.XXXXXXXXXX)
- prepare_build_directory
-
- info "Downloading JuJu..."
- builtin cd ${maindir}
- local imagefile=juju-${ARCH}.tar.gz
- $WGET ${JUJU_REPO}/${imagefile}
-
- info "Installing JuJu..."
- _setup_juju ${maindir}/${imagefile}
-
- cleanup_build_directory ${maindir}
-}
-
-
-function setup_from_file_juju(){
-# Setup from file the JuJu environment
- [ "$JUJU_ENV" == "1" ] && die "Error: The operation is not allowed inside JuJu environment"
-
- local imagefile=$1
- [ ! -e ${imagefile} ] && die "Error: The JuJu image file ${imagefile} does not exist"
-
- info "Installing JuJu from ${imagefile}..."
- _setup_juju ${ORIGIN_WD}/${imagefile}
-
- builtin cd $ORIGIN_WD
-}
-
-
-function run_juju_as_root(){
- [ "$JUJU_ENV" == "1" ] && die "Error: The operation is not allowed inside JuJu environment"
-
- mkdir -p ${JUJU_HOME}/${HOME}
- ${JUJU_HOME}/usr/bin/arch-chroot $JUJU_HOME /usr/bin/bash -c 'mkdir -p /run/lock && /bin/sh'
-}
-
-
-function _run_juju_with_proot(){
- if ${PROOT} ${JUJU_HOME}/usr/bin/true &> /dev/null
- then
- JUJU_ENV=1 ${PROOT} $@ ${JUJU_HOME} ${SH}
- else
- JUJU_ENV=1 PROOT_NO_SECCOMP=1 ${PROOT} $@ ${JUJU_HOME} ${SH}
- fi
-}
-
-
-function run_juju_as_fakeroot(){
- _run_juju_with_proot "-S"
-}
-
-
-function run_juju_as_user(){
- _run_juju_with_proot "-R"
-}
-
-
-function delete_juju(){
- [ "$JUJU_ENV" == "1" ] && die "Error: The operation is not allowed inside JuJu environment"
-
- ! ask "Are you sure to delete JuJu located in ${JUJU_HOME}" "N" && return
- if mountpoint -q ${JUJU_HOME}
- then
- info "There are mounted directories inside ${JUJU_HOME}"
- if ! umount --force ${JUJU_HOME}
- then
- error "Cannot umount directories in ${JUJU_HOME}"
- die "Try to delete juju using root permissions"
- fi
- fi
- if rm -rf ${JUJU_HOME}/*
- then
- info "JuJu deleted in ${JUJU_HOME}"
- else
- error "Error: Cannot delete JuJu in ${JUJU_HOME}"
- fi
-}
-
-
-function _check_package(){
- if ! pacman -Qq $1 > /dev/null
- then
- die "Package $1 must be installed"
- fi
-}
-
-
-function build_image_juju(){
-# The function must runs on ArchLinux
-# The dependencies are:
-# arch-install-scripts
-# base-devel
-# package-query
-# git
- _check_package arch-install-scripts
- _check_package gcc
- _check_package package-query
- _check_package git
- local maindir=$(TMPDIR=$JUJU_TEMPDIR mktemp -d -t juju.XXXXXXXXXX)
- mkdir -p ${maindir}/root
- prepare_build_directory
- info "Installing pacman and its dependencies..."
- pacstrap -d ${maindir}/root pacman arch-install-scripts binutils libunistring
-
- info "Generating the locales..."
- ln -sf /usr/share/zoneinfo/posix/UTC ${maindir}/root/etc/localtime
- echo "en_US.UTF-8 UTF-8" >> ${maindir}/root/etc/locale.gen
- arch-chroot ${maindir}/root locale-gen
- echo 'LANG = "en_US.UTF-8"' >> ${maindir}/root/etc/locale.conf
-
- info "Compiling and installing yaourt..."
- mkdir -p ${maindir}/packages/{package-query,yaourt,proot}
-
- builtin cd ${maindir}/packages/package-query
- $WGET https://aur.archlinux.org/packages/pa/package-query/PKGBUILD
- makepkg -sfc --asroot
- pacman --noconfirm --root ${maindir}/root -U package-query*.pkg.tar.xz
-
- builtin cd ${maindir}/packages/yaourt
- $WGET https://aur.archlinux.org/packages/ya/yaourt/PKGBUILD
- makepkg -sfc --asroot
- pacman --noconfirm --root ${maindir}/root -U yaourt*.pkg.tar.xz
-
- info "Compiling and installing proot..."
- builtin cd ${maindir}/packages/proot
- $WGET https://aur.archlinux.org/packages/pr/proot/PKGBUILD
- makepkg -sfcA --asroot
- pacman --noconfirm --root ${maindir}/root -U proot*.pkg.tar.xz
-
- rm ${maindir}/root/var/cache/pacman/pkg/*
-
- info "Copying JuJu scripts..."
- git clone https://github.com/fsquillace/juju.git ${maindir}/root/opt/juju
- echo 'export PATH=$PATH:/opt/juju/bin' > ${maindir}/root/etc/profile.d/juju.sh
- chmod +x ${maindir}/root/etc/profile.d/juju.sh
-
- builtin cd ${ORIGIN_WD}
- local imagefile="juju-${ARCH}.tar.gz"
- info "Compressing image to ${imagefile}..."
- tar -zcpf ${imagefile} -C ${maindir}/root .
- cleanup_build_directory ${maindir}
-}
diff --git a/lib/core/build.sh b/lib/core/build.sh
new file mode 100644
index 0000000..65a9474
--- /dev/null
+++ b/lib/core/build.sh
@@ -0,0 +1,141 @@
+#!/usr/bin/env bash
+#
+# This module contains all build functionalities for JuNest.
+#
+# Dependencies:
+# - lib/utils/utils.sh
+# - lib/core/common.sh
+#
+# vim: ft=sh
+
+function _install_pkg(){
+ # This function allows to install packages from AUR.
+ # At the moment is not used.
+ local maindir=$1
+ local pkgbuilddir=$2
+ # Generate a working directory because sources will be downloaded to there
+ working_dir=$(TMPDIR=/tmp mktemp -d -t junest-wd.XXXXXXXXXX)
+ cp -R "$pkgbuilddir"/* "$working_dir"
+ builtin cd "${working_dir}" || return 1
+ makepkg -sfcd
+ makepkg --printsrcinfo > "${pkgbuilddir}"/.SRCINFO
+ sudo pacman --noconfirm --root "${maindir}"/root -U ./*.pkg.tar.*
+}
+
+function _prepare() {
+ # ArchLinux System initialization
+ prepare_archlinux
+ # curl is used to download pacman.conf file
+ sudo pacman -S --noconfirm git arch-install-scripts haveged curl
+}
+
+function build_image_env(){
+ set -x
+ umask 022
+
+ # The function must runs on ArchLinux with non-root privileges.
+ # This is because installing AUR packages can be done by normal users only.
+ (( EUID == 0 )) && \
+ die "You cannot build with root privileges."
+
+ _prepare
+
+ local disable_validation=$1
+
+ local maindir
+ maindir=$(TMPDIR=$JUNEST_TEMPDIR mktemp -d -t "${CMD}".XXXXXXXXXX)
+ sudo mkdir -p "${maindir}"/root
+ trap - QUIT EXIT ABRT TERM INT
+ # shellcheck disable=SC2064
+ trap "sudo rm -rf ${maindir}; die \"Error occurred when installing ${NAME}\"" EXIT QUIT ABRT TERM INT
+ info "Installing pacman and its dependencies..."
+ # All the essential executables (ln, mkdir, chown, etc) are in coreutils
+ # bwrap command belongs to bubblewrap
+ sudo pacstrap -G -M "${maindir}"/root pacman coreutils bubblewrap
+
+ if [[ ${ARCH} != "arm" ]]
+ then
+ # x86_64 does not have any mirror set by default...
+ sudo bash -c "echo 'Server = $DEFAULT_MIRROR' >> ${maindir}/root/etc/pacman.d/mirrorlist"
+ fi
+ sudo mkdir -p "${maindir}"/root/run/lock
+
+ # For some reasons, pacstrap does not create the pacman.conf file,
+ # I could not reproduce the issue locally though:
+ # https://app.travis-ci.com/github/fsquillace/junest/builds/268216346
+ [[ -e "${maindir}"/root/etc/pacman.conf ]] || sudo curl "https://gitlab.archlinux.org/archlinux/packaging/packages/pacman/-/raw/main/pacman.conf" -o "${maindir}/root/etc/pacman.conf"
+
+ # Pacman/pacstrap bug: https://gitlab.archlinux.org/archlinux/packaging/packages/arch-install-scripts/-/issues/3
+ sudo sed -i '/^DownloadUser = alpm$/d' "${maindir}"/root/etc/pacman.conf
+
+ sudo tee -a "${maindir}"/root/etc/pacman.conf < ${maindir}/root/etc/${CMD}/info"
+ # Related to: https://github.com/fsquillace/junest/issues/305
+ sudo bash -c "echo 'export FAKEROOTDONTTRYCHOWN=true' > ${maindir}/root/etc/profile.d/junest.sh"
+
+ info "Generating the locales..."
+ # sed command is required for locale-gen but it is required by fakeroot
+ # and cannot be removed
+ # localedef (called by locale-gen) requires gzip but it is supposed to be
+ # already installed as systemd already depends on it
+ sudo pacman --noconfirm --root "${maindir}"/root -S sed gzip
+ sudo ln -sf /usr/share/zoneinfo/posix/UTC "${maindir}"/root/etc/localtime
+ sudo bash -c "echo 'en_US.UTF-8 UTF-8' >> ${maindir}/root/etc/locale.gen"
+ sudo "${maindir}"/root/bin/groot "${maindir}"/root locale-gen
+ sudo bash -c "echo LANG=\"en_US.UTF-8\" >> ${maindir}/root/etc/locale.conf"
+
+ info "Setting up the pacman keyring (this might take a while!)..."
+ if [[ $(uname -m) == *"arm"* ]]
+ then
+ sudo pacman -S --noconfirm --root "${maindir}"/root archlinuxarm-keyring
+ else
+ sudo pacman -S --noconfirm --root "${maindir}"/root archlinux-keyring
+ fi
+ sudo mount --bind "${maindir}"/root "${maindir}"/root
+ sudo arch-chroot "${maindir}"/root bash -c '
+ set -e
+ pacman-key --init;
+ for keyring_file in /usr/share/pacman/keyrings/*.gpg;
+ do
+ keyring=$(basename $keyring_file | cut -f 1 -d ".");
+ pacman-key --populate $keyring;
+ done;'
+ sudo umount "${maindir}"/root
+
+ sudo rm "${maindir}"/root/var/cache/pacman/pkg/*
+ # This is needed on system with busybox tar command.
+ # If the file does not have write permission, the tar command to extract files fails.
+ sudo chmod -R u+rw "${maindir}"/root/
+
+ mkdir -p "${maindir}"/output
+ builtin cd "${maindir}"/output || return 1
+ local imagefile="${CMD}-${ARCH}.tar.gz"
+ info "Compressing image to ${imagefile}..."
+ sudo "$TAR" -zcpf "${imagefile}" -C "${maindir}"/root .
+
+ if ! $disable_validation
+ then
+ mkdir -p "${maindir}"/root_test
+ $TAR -zxpf "${imagefile}" -C "${maindir}/root_test"
+ JUNEST_HOME="${maindir}/root_test" "${JUNEST_BASE}"/lib/checks/check_all.sh
+ fi
+
+ sudo cp "${maindir}"/output/"${imagefile}" "${ORIGIN_WD}"
+
+ builtin cd "${ORIGIN_WD}" || return 1
+ trap - QUIT EXIT ABRT KILL TERM INT
+ sudo rm -fr "$maindir"
+
+ set +x
+}
diff --git a/lib/core/chroot.sh b/lib/core/chroot.sh
new file mode 100644
index 0000000..c2237a9
--- /dev/null
+++ b/lib/core/chroot.sh
@@ -0,0 +1,104 @@
+#!/usr/bin/env bash
+#
+# This module contains all chroot functionalities for JuNest.
+#
+# Dependencies:
+# - lib/utils/utils.sh
+# - lib/core/common.sh
+#
+# vim: ft=sh
+
+function _run_env_as_xroot(){
+ local cmd=$1
+ local backend_args="$2"
+ local no_copy_files="$3"
+ shift 3
+
+ check_same_arch
+
+ local uid=$UID
+ # SUDO_USER is more reliable compared to SUDO_UID
+ [[ -z $SUDO_USER ]] || uid=$SUDO_USER:$SUDO_GID
+
+ local args=()
+ [[ "$1" != "" ]] && args=("-c" "$(insert_quotes_on_spaces "${@}")")
+
+ # With chown the ownership of the files is assigned to the real user
+ trap - QUIT EXIT ABRT KILL TERM INT
+ # shellcheck disable=SC2064
+ trap "[ -z $uid ] || chown_cmd -R ${uid} ${JUNEST_HOME};" EXIT QUIT ABRT TERM INT
+
+ if ! $no_copy_files
+ then
+ copy_common_files
+ fi
+
+ # shellcheck disable=SC2086
+ JUNEST_ENV=1 $cmd $backend_args "$JUNEST_HOME" "${DEFAULT_SH[@]}" "${args[@]}"
+}
+
+#######################################
+# Run JuNest as real root via GRoot command.
+#
+# Globals:
+# JUNEST_HOME (RO) : The JuNest home directory.
+# UID (RO) : The user ID.
+# SUDO_USER (RO) : The sudo user ID.
+# SUDO_GID (RO) : The sudo group ID.
+# DEFAULT_SH (RO) : Contains the default command to run in JuNest.
+# Arguments:
+# backend_args ($1) : The arguments to pass to backend program
+# no_copy_files ($2?) : If false it will copy some files in /etc
+# from host to JuNest environment.
+# cmd ($3-?) : The command to run inside JuNest environment.
+# Default command is defined by DEFAULT_SH variable.
+# Returns:
+# $ARCHITECTURE_MISMATCH : If host and JuNest architecture are different.
+# Output:
+# - : The command output.
+#######################################
+function run_env_as_groot(){
+ check_nested_env
+
+ local backend_command="${1:-$GROOT}"
+ local backend_args="$2"
+ local no_copy_files="$3"
+ shift 3
+
+ provide_common_bindings
+ local bindings=${RESULT}
+ unset RESULT
+
+ _run_env_as_xroot "$backend_command $bindings" "$backend_args" "$no_copy_files" "$@"
+}
+
+#######################################
+# Run JuNest as real root via chroot command.
+#
+# Globals:
+# JUNEST_HOME (RO) : The JuNest home directory.
+# UID (RO) : The user ID.
+# SUDO_USER (RO) : The sudo user ID.
+# SUDO_GID (RO) : The sudo group ID.
+# DEFAULT_SH (RO) : Contains the default command to run in JuNest.
+# Arguments:
+# backend_args ($1) : The arguments to pass to backend program
+# no_copy_files ($2?) : If false it will copy some files in /etc
+# from host to JuNest environment.
+# cmd ($3-?) : The command to run inside JuNest environment.
+# Default command is defined by DEFAULT_SH variable.
+# Returns:
+# $ARCHITECTURE_MISMATCH : If host and JuNest architecture are different.
+# Output:
+# - : The command output.
+#######################################
+function run_env_as_chroot(){
+ check_nested_env
+
+ local backend_command="${1:-chroot_cmd}"
+ local backend_args="$2"
+ local no_copy_files="$3"
+ shift 3
+
+ _run_env_as_xroot "$backend_command" "$backend_args" "$no_copy_files" "$@"
+}
diff --git a/lib/core/common.sh b/lib/core/common.sh
new file mode 100644
index 0000000..df79bec
--- /dev/null
+++ b/lib/core/common.sh
@@ -0,0 +1,335 @@
+#!/usr/bin/env bash
+# shellcheck disable=SC2034
+# shellcheck disable=SC1091
+#
+# This module contains all common functionalities for JuNest.
+#
+# Dependencies:
+# - lib/utils/utils.sh
+#
+# vim: ft=sh
+
+NAME='JuNest'
+CMD='junest'
+DESCRIPTION='The Arch Linux based distro that runs upon any Linux distros without root access'
+
+NOT_AVAILABLE_ARCH=102
+NOT_EXISTING_FILE=103
+ARCHITECTURE_MISMATCH=104
+ROOT_ACCESS_ERROR=105
+NESTED_ENVIRONMENT=106
+VARIABLE_NOT_SET=107
+NO_CONFIG_FOUND=108
+UNPRIVILEGED_USERNS_DISABLED=109
+
+JUNEST_HOME=${JUNEST_HOME:-~/.${CMD}}
+JUNEST_TEMPDIR=${JUNEST_TEMPDIR:-/tmp}
+
+# The update of the variable PATH ensures that the executables are
+# found on different locations
+PATH=/usr/bin:/bin:/usr/local/bin:/usr/sbin:/sbin:${HOME}/.local/bin:"$PATH"
+
+# The executable uname is essential in order to get the architecture
+# of the host system, so a fallback mechanism cannot be used for it.
+UNAME="uname"
+
+ARCH_LIST=('x86_64' 'x86' 'arm')
+HOST_ARCH=$($UNAME -m)
+# To check all available architectures look here:
+# https://wiki.archlinux.org/index.php/PKGBUILD#arch
+if [[ $HOST_ARCH == "i686" ]] || [[ $HOST_ARCH == "i386" ]]
+then
+ ARCH="x86"
+ LD_LIB="${JUNEST_HOME}/lib/ld-linux.so.2"
+elif [[ $HOST_ARCH == "x86_64" ]]
+then
+ ARCH="x86_64"
+ LD_LIB="${JUNEST_HOME}/lib64/ld-linux-x86-64.so.2"
+elif [[ $HOST_ARCH =~ .*(arm).* ]] || [[ $HOST_ARCH == "aarch64" ]]
+then
+ ARCH="arm"
+ LD_LIB="${JUNEST_HOME}/lib/ld-linux-armhf.so.3"
+else
+ die "Unknown architecture ${HOST_ARCH}"
+fi
+
+MAIN_REPO=https://link.storjshare.io/s/jvb5tgarnjtt565fffa44spvyuga/junest-repo
+MAIN_REPO=https://pub-a2af2344e8554f6c807bc3db355ae622.r2.dev
+ENV_REPO=${MAIN_REPO}/${CMD}
+# shellcheck disable=SC2016
+DEFAULT_MIRROR='https://mirror.rackspace.com/archlinux/$repo/os/$arch'
+
+ORIGIN_WD=$(pwd)
+
+################## EXECUTABLES ################
+
+# This section contains all the executables needed for JuNest to run properly.
+# They are based on a fallback mechanism that tries to use the executable in
+# different locations in the host OS.
+
+# List of executables that are run inside JuNest:
+DEFAULT_SH=("/bin/sh" "--login")
+
+# List of executables that are run in the host OS:
+BWRAP="${JUNEST_HOME}/usr/bin/bwrap"
+PROOT="${JUNEST_HOME}/usr/bin/proot-${ARCH}"
+GROOT="${JUNEST_HOME}/usr/bin/groot"
+CLASSIC_CHROOT=chroot
+WGET="wget --content-disposition --no-check-certificate"
+CURL="curl -L -J -O -k"
+TAR="tar"
+CHOWN="chown"
+LN="ln"
+RM="rm"
+MKDIR="mkdir"
+GETENT="getent"
+CP="cp"
+ID="id"
+# Used for checking user namespace in config.gz file
+ZGREP="zgrep"
+UNSHARE="unshare"
+
+LD_EXEC="$LD_LIB --library-path ${JUNEST_HOME}/usr/lib:${JUNEST_HOME}/lib"
+
+# The following functions attempt first to run the executable in the host OS.
+# As a last hope they try to run the same executable available in the JuNest
+# image.
+
+function ln_cmd(){
+ $LN "$@" || $LD_EXEC "${JUNEST_HOME}"/usr/bin/$LN "$@"
+}
+
+function getent_cmd(){
+ $GETENT "$@" || $LD_EXEC "${JUNEST_HOME}"/usr/bin/$GETENT "$@"
+}
+
+function cp_cmd(){
+ $CP "$@" || $LD_EXEC "${JUNEST_HOME}"/usr/bin/$CP "$@"
+}
+
+function rm_cmd(){
+ $RM "$@" || $LD_EXEC "${JUNEST_HOME}"/usr/bin/$RM "$@"
+}
+
+function chown_cmd(){
+ $CHOWN "$@" || $LD_EXEC "${JUNEST_HOME}"/usr/bin/$CHOWN "$@"
+}
+
+function mkdir_cmd(){
+ $MKDIR "$@" || $LD_EXEC "${JUNEST_HOME}"/usr/bin/$MKDIR "$@"
+}
+
+function zgrep_cmd(){
+ # No need for LD_EXEC as zgrep is a POSIX shell script
+ $ZGREP "$@" || "${JUNEST_HOME}"/usr/bin/$ZGREP "$@"
+}
+
+function download_cmd(){
+ $WGET "$@" || $CURL "$@"
+}
+
+function chroot_cmd(){
+ $CLASSIC_CHROOT "$@" || $LD_EXEC "${JUNEST_HOME}"/usr/bin/$CLASSIC_CHROOT "$@"
+}
+
+function unshare_cmd(){
+ # Most of the distros do not have the `unshare` command updated
+ # with --user option available.
+ # Hence, give priority to the `unshare` executable in JuNest image.
+ # Also, unshare provides an environment in which /bin/sh maps to dash shell,
+ # therefore it ignores all the remaining DEFAULT_SH arguments (i.e. --login) as
+ # they are not supported by dash.
+ if $LD_EXEC "${JUNEST_HOME}"/usr/bin/$UNSHARE --user "${DEFAULT_SH[0]}" "-c" ":"
+ then
+ $LD_EXEC "${JUNEST_HOME}"/usr/bin/$UNSHARE "${@}"
+ elif $UNSHARE --user "${DEFAULT_SH[0]}" "-c" ":"
+ then
+ $UNSHARE "$@"
+ else
+ die "Error: Something went wrong while executing unshare command. Exiting"
+ fi
+}
+
+function bwrap_cmd(){
+ if $LD_EXEC "$BWRAP" --dev-bind / / "${DEFAULT_SH[0]}" "-c" ":"
+ then
+ $LD_EXEC "$BWRAP" "${@}"
+ else
+ die "Error: Something went wrong while executing bwrap command. Exiting"
+ fi
+}
+
+function proot_cmd(){
+ local proot_args="$1"
+ shift
+ # shellcheck disable=SC2086
+ if ${PROOT} ${proot_args} "${DEFAULT_SH[@]}" "-c" ":"
+ then
+ # shellcheck disable=SC2086
+ ${PROOT} ${proot_args} "${@}"
+ elif PROOT_NO_SECCOMP=1 ${PROOT} ${proot_args} "${DEFAULT_SH[@]}" "-c" ":"
+ then
+ warn "Warn: Proot is not properly working. Disabling SECCOMP and expect the application to run slowly in particular when it uses syscalls intensively."
+ warn "Try to use Linux namespace instead as it is more reliable: junest ns"
+ PROOT_NO_SECCOMP=1 ${PROOT} ${proot_args} "${@}"
+ else
+ die "Error: Something went wrong with proot command. Exiting"
+ fi
+}
+
+############## COMMON FUNCTIONS ###############
+
+#######################################
+# Check if the executable is being running inside a JuNest environment.
+#
+# Globals:
+# JUNEST_ENV (RO) : The boolean junest env check
+# NESTED_ENVIRONMENT (RO) : The nest env exception
+# VARIABLE_NOT_SET (RO) : The var not set exception
+# NAME (RO) : The JuNest name
+# Arguments:
+# None
+# Returns:
+# VARIABLE_NOT_SET : If no JUNEST_ENV is not properly set
+# NESTED_ENVIRONMENT : If the script is executed inside JuNest env
+# Output:
+# None
+#######################################
+function check_nested_env() {
+ if [[ $JUNEST_ENV == "1" ]]
+ then
+ die_on_status $NESTED_ENVIRONMENT "Error: Nested ${NAME} environments are not allowed"
+ elif [[ -n $JUNEST_ENV ]] && [[ $JUNEST_ENV != "0" ]]
+ then
+ die_on_status $VARIABLE_NOT_SET "The variable JUNEST_ENV is not properly set"
+ fi
+}
+
+#######################################
+# Check if the architecture between Host OS and Guest OS is the same.
+#
+# Globals:
+# JUNEST_HOME (RO) : The JuNest home path.
+# ARCHITECTURE_MISMATCH (RO) : The arch mismatch exception
+# ARCH (RO) : The host OS arch
+# JUNEST_ARCH (RO) : The JuNest arch
+# Arguments:
+# None
+# Returns:
+# ARCHITECTURE_MISMATCH : If arch between host and guest is not the same
+# Output:
+# None
+#######################################
+function check_same_arch() {
+ source "${JUNEST_HOME}"/etc/junest/info
+ [ "$JUNEST_ARCH" != "$ARCH" ] && \
+ die_on_status $ARCHITECTURE_MISMATCH "The host system architecture is not correct: $ARCH != $JUNEST_ARCH"
+ return 0
+}
+
+#######################################
+# Provide the proot common binding options for both normal user and fakeroot.
+# The list of bindings can be found in `proot --help`. This function excludes
+# /etc/mtab file so that it will not give conflicts with the related
+# symlink in the image.
+#
+# Globals:
+# HOME (RO) : The home directory.
+# RESULT (WO) : Contains the binding options.
+# Arguments:
+# None
+# Returns:
+# None
+# Output:
+# None
+#######################################
+function provide_common_bindings(){
+ RESULT=""
+ local re='(.*):.*'
+ for bind in "/dev" "/sys" "/proc" "/tmp" "$HOME" "/run/user/$($ID -u)"
+ do
+ if [[ $bind =~ $re ]]
+ then
+ [ -e "${BASH_REMATCH[1]}" ] && RESULT="-b $bind $RESULT"
+ else
+ [ -e "$bind" ] && RESULT="-b $bind $RESULT"
+ fi
+ done
+ return 0
+}
+
+#######################################
+# Build passwd and group files using getent command.
+# If getent fails the function fallbacks by copying the content from /etc/passwd
+# and /etc/group.
+#
+# The generated passwd and group will be stored in $JUNEST_HOME/etc/junest.
+#
+# Globals:
+# JUNEST_HOME (RO) : The JuNest home directory.
+# Arguments:
+# None
+# Returns:
+# None
+# Output:
+# None
+#######################################
+function copy_passwd_and_group(){
+ # Enumeration of users/groups is disabled/limited depending on how nsswitch.conf
+ # is configured.
+ # Try to at least get the current user via `getent passwd $USER` since it uses
+ # a more reliable and faster system call (getpwnam(3)).
+ if ! getent_cmd passwd > "${JUNEST_HOME}"/etc/passwd || \
+ ! getent_cmd passwd "${USER}" >> "${JUNEST_HOME}"/etc/passwd
+ then
+ warn "getent command failed or does not exist. Binding directly from /etc/passwd."
+ copy_file /etc/passwd
+ fi
+
+ if ! getent_cmd group > "${JUNEST_HOME}"/etc/group
+ then
+ warn "getent command failed or does not exist. Binding directly from /etc/group."
+ copy_file /etc/group
+ fi
+ return 0
+}
+
+function copy_file() {
+ local file="${1}"
+ # -f option ensure to remove destination file if it cannot be opened
+ # https://github.com/fsquillace/junest/issues/284
+ [[ -r "$file" ]] && cp_cmd -f "$file" "${JUNEST_HOME}/$file"
+ return 0
+}
+
+function copy_common_files() {
+ copy_file /etc/host.conf
+ copy_file /etc/hosts
+ copy_file /etc/nsswitch.conf
+ copy_file /etc/resolv.conf
+ return 0
+}
+
+function prepare_archlinux() {
+ local sudo=${1:-sudo}
+ local pacman_options="--noconfirm --disable-download-timeout"
+
+ # shellcheck disable=SC2086
+ $sudo pacman $pacman_options -Syy
+
+ $sudo pacman-key --init
+
+ if [[ $(uname -m) == *"arm"* ]]
+ then
+ # shellcheck disable=SC2086
+ $sudo pacman $pacman_options -S archlinuxarm-keyring
+ $sudo pacman-key --populate archlinuxarm
+ else
+ # shellcheck disable=SC2086
+ $sudo pacman $pacman_options -S archlinux-keyring
+ $sudo pacman-key --populate archlinux
+ fi
+
+ # shellcheck disable=SC2086
+ $sudo pacman $pacman_options -Su
+}
diff --git a/lib/core/namespace.sh b/lib/core/namespace.sh
new file mode 100644
index 0000000..70763bd
--- /dev/null
+++ b/lib/core/namespace.sh
@@ -0,0 +1,165 @@
+#!/usr/bin/env bash
+#
+# This module contains functionalities for accessing to JuNest via bubblewrap.
+#
+# https://github.com/containers/bubblewrap
+#
+# Dependencies:
+# - lib/utils/utils.sh
+# - lib/core/common.sh
+#
+# vim: ft=sh
+
+# shellcheck disable=SC2027
+COMMON_BWRAP_OPTION="--bind "$JUNEST_HOME" / --bind "$HOME" "$HOME" --bind /tmp /tmp --bind /sys /sys --bind /proc /proc --dev-bind-try /dev /dev --bind-try "/run/user/$($ID -u)" "/run/user/$($ID -u)" --unshare-user-try"
+CONFIG_PROC_FILE="/proc/config.gz"
+CONFIG_BOOT_FILE="/boot/config-$($UNAME -r)"
+PROC_USERNS_CLONE_FILE="/proc/sys/kernel/unprivileged_userns_clone"
+PROC_USERNS_FILE="/proc/$$/ns/user"
+
+function _is_user_namespace_enabled() {
+ if [[ -L $PROC_USERNS_FILE ]]
+ then
+ return 0
+ fi
+
+ if [[ -e $PROC_USERNS_CLONE_FILE ]]
+ then
+ # `-q` option in zgrep may cause a gzip: stdout: Broken pipe
+ # Use redirect to /dev/null instead
+ if zgrep_cmd "1" "$PROC_USERNS_CLONE_FILE" > /dev/null
+ then
+ return 0
+ fi
+ fi
+
+ local config_file=""
+ if [[ -e $CONFIG_PROC_FILE ]]
+ then
+ config_file=$CONFIG_PROC_FILE
+ elif [[ -e $CONFIG_BOOT_FILE ]]
+ then
+ config_file=$CONFIG_BOOT_FILE
+ else
+ return "$NOT_EXISTING_FILE"
+ fi
+
+ # `-q` option in zgrep may cause a gzip: stdout: Broken pipe
+ # Use redirect to /dev/null instead
+ if ! zgrep_cmd "CONFIG_USER_NS=y" "$config_file" > /dev/null
+ then
+ return "$NO_CONFIG_FOUND"
+ fi
+
+ return "$UNPRIVILEGED_USERNS_DISABLED"
+}
+
+function _check_user_namespace() {
+ set +e
+ _is_user_namespace_enabled
+ case $? in
+ "$NOT_EXISTING_FILE") warn "Could not understand if user namespace is enabled. No config.gz file found. Proceeding anyway..." ;;
+ "$NO_CONFIG_FOUND") warn "Unprivileged user namespace is disabled at kernel compile time or kernel too old (<3.8). Proceeding anyway..." ;;
+ "$UNPRIVILEGED_USERNS_DISABLED") warn "Unprivileged user namespace disabled. Root permissions are required to enable it: sudo sysctl kernel.unprivileged_userns_clone=1" ;;
+ esac
+ set -e
+}
+
+
+#######################################
+# Run JuNest as fakeroot via bwrap
+#
+# Globals:
+# JUNEST_HOME (RO) : The JuNest home directory.
+# DEFAULT_SH (RO) : Contains the default command to run in JuNest.
+# BWRAP (RO): : The location of the bwrap binary.
+# Arguments:
+# backend_args ($1) : The arguments to pass to bwrap
+# no_copy_files ($2?) : If false it will copy some files in /etc
+# from host to JuNest environment.
+# cmd ($3-?) : The command to run inside JuNest environment.
+# Default command is defined by DEFAULT_SH variable.
+# Returns:
+# $ARCHITECTURE_MISMATCH : If host and JuNest architecture are different.
+# $ROOT_ACCESS_ERROR : If the user is the real root.
+# Output:
+# - : The command output.
+#######################################
+function run_env_as_bwrap_fakeroot(){
+ check_nested_env
+
+ local backend_command="${1:-$BWRAP}"
+ local backend_args="$2"
+ local no_copy_files="$3"
+ shift 3
+
+ _check_user_namespace
+
+ check_same_arch
+
+ if ! $no_copy_files
+ then
+ copy_common_files
+ fi
+
+ local args=()
+ [[ "$1" != "" ]] && args=("-c" "$(insert_quotes_on_spaces "${@}")")
+
+ # Fix PATH to /usr/bin to make sudo working and avoid polluting with host related bin paths
+ # shellcheck disable=SC2086
+ PATH="/usr/bin" BWRAP="${backend_command}" JUNEST_ENV=1 bwrap_cmd $COMMON_BWRAP_OPTION --cap-add ALL --uid 0 --gid 0 $backend_args sudo "${DEFAULT_SH[@]}" "${args[@]}"
+}
+
+
+#######################################
+# Run JuNest as normal user via bwrap.
+#
+# Globals:
+# JUNEST_HOME (RO) : The JuNest home directory.
+# DEFAULT_SH (RO) : Contains the default command to run in JuNest.
+# BWRAP (RO): : The location of the bwrap binary.
+# Arguments:
+# backend_args ($1) : The arguments to pass to bwrap
+# no_copy_files ($2?) : If false it will copy some files in /etc
+# from host to JuNest environment.
+# cmd ($3-?) : The command to run inside JuNest environment.
+# Default command is defined by DEFAULT_SH variable.
+# Returns:
+# $ARCHITECTURE_MISMATCH : If host and JuNest architecture are different.
+# Output:
+# - : The command output.
+#######################################
+function run_env_as_bwrap_user() {
+ check_nested_env
+
+ local backend_command="${1:-$BWRAP}"
+ local backend_args="$2"
+ local no_copy_files="$3"
+ shift 3
+
+ _check_user_namespace
+
+ check_same_arch
+
+ if ! $no_copy_files
+ then
+ copy_common_files
+ copy_file /etc/hosts.equiv
+ copy_file /etc/netgroup
+ copy_file /etc/networks
+ # No need for localtime as it is setup during the image build
+ #copy_file /etc/localtime
+ copy_passwd_and_group
+ fi
+
+ local args=()
+ [[ "$1" != "" ]] && args=("-c" "$(insert_quotes_on_spaces "${@}")")
+
+ # Resets PATH to avoid polluting with host related bin paths
+ # shellcheck disable=SC2086
+ PATH='' BWRAP="${backend_command}" JUNEST_ENV=1 bwrap_cmd $COMMON_BWRAP_OPTION $backend_args "${DEFAULT_SH[@]}" "${args[@]}"
+}
+
+
+
+
diff --git a/lib/core/proot.sh b/lib/core/proot.sh
new file mode 100644
index 0000000..b6c1c8f
--- /dev/null
+++ b/lib/core/proot.sh
@@ -0,0 +1,138 @@
+#!/usr/bin/env bash
+# shellcheck disable=SC1091
+#
+# This module contains all proot functionalities for JuNest.
+#
+# Dependencies:
+# - lib/utils/utils.sh
+# - lib/core/common.sh
+#
+# vim: ft=sh
+
+function _run_env_with_proot(){
+ local backend_command="${1:-$PROOT}"
+ local backend_args="$2"
+ shift 2
+
+ local args=()
+ [[ "$1" != "" ]] && args=("-c" "$(insert_quotes_on_spaces "${@}")")
+
+ # Resets PATH to avoid polluting with host related bin paths
+ PATH='' PROOT="${backend_command}" JUNEST_ENV=1 proot_cmd "${backend_args}" "${DEFAULT_SH[@]}" "${args[@]}"
+}
+
+function _run_env_with_qemu(){
+ local backend_command="$1"
+ local backend_args="$2"
+ shift 2
+
+ source "${JUNEST_HOME}"/etc/junest/info
+
+ if [ "$JUNEST_ARCH" != "$ARCH" ]
+ then
+ local qemu_bin="qemu-$JUNEST_ARCH-static-$ARCH"
+ local qemu_symlink="/tmp/${qemu_bin}-$RANDOM"
+ trap - QUIT EXIT ABRT KILL TERM INT
+ # shellcheck disable=SC2064
+ trap "[ -e ${qemu_symlink} ] && rm_cmd -f ${qemu_symlink}" EXIT QUIT ABRT TERM INT
+
+ warn "Emulating $NAME via QEMU..."
+ [[ -e ${qemu_symlink} ]] || \
+ ln_cmd -s "${JUNEST_HOME}/bin/${qemu_bin}" "${qemu_symlink}"
+ backend_args="-q ${qemu_symlink} $backend_args"
+ fi
+
+ _run_env_with_proot "${backend_command}" "$backend_args" "${@}"
+}
+
+#######################################
+# Run JuNest as fakeroot.
+#
+# Globals:
+# JUNEST_HOME (RO) : The JuNest home directory.
+# EUID (RO) : The user ID.
+# DEFAULT_SH (RO) : Contains the default command to run in JuNest.
+# Arguments:
+# backend_args ($1) : The arguments to pass to proot
+# no_copy_files ($2?) : If false it will copy some files in /etc
+# from host to JuNest environment.
+# cmd ($3-?) : The command to run inside JuNest environment.
+# Default command is defined by DEFAULT_SH variable.
+# Returns:
+# $ROOT_ACCESS_ERROR : If the user is the real root.
+# Output:
+# - : The command output.
+#######################################
+function run_env_as_proot_fakeroot(){
+ (( EUID == 0 )) && \
+ die_on_status "$ROOT_ACCESS_ERROR" "You cannot access with root privileges. Use --groot option instead."
+ check_nested_env
+
+ local backend_command="$1"
+ local backend_args="$2"
+ local no_copy_files="$3"
+ shift 3
+
+ if ! $no_copy_files
+ then
+ copy_common_files
+ fi
+
+ provide_common_bindings
+ local bindings=${RESULT}
+ unset RESULT
+
+ # An alternative is via -S option:
+ #_run_env_with_qemu "-S ${JUNEST_HOME} $1" "${@:2}"
+ _run_env_with_qemu "$backend_command" "-0 ${bindings} -r ${JUNEST_HOME} $backend_args" "$@"
+}
+
+#######################################
+# Run JuNest as normal user.
+#
+# Globals:
+# JUNEST_HOME (RO) : The JuNest home directory.
+# EUID (RO) : The user ID.
+# DEFAULT_SH (RO) : Contains the default command to run in JuNest.
+# Arguments:
+# backend_args ($1) : The arguments to pass to proot
+# no_copy_files ($2?) : If false it will copy some files in /etc
+# from host to JuNest environment.
+# cmd ($3-?) : The command to run inside JuNest environment.
+# Default command is defined by DEFAULT_SH variable.
+# Returns:
+# $ROOT_ACCESS_ERROR : If the user is the real root.
+# Output:
+# - : The command output.
+#######################################
+function run_env_as_proot_user(){
+ (( EUID == 0 )) && \
+ die_on_status "$ROOT_ACCESS_ERROR" "You cannot access with root privileges. Use --groot option instead."
+ check_nested_env
+
+ local backend_command="$1"
+ local backend_args="$2"
+ local no_copy_files="$3"
+ shift 3
+
+ if ! $no_copy_files
+ then
+ # Files to bind are visible in `proot --help`.
+ # This function excludes /etc/mtab file so that
+ # it will not give conflicts with the related
+ # symlink in the Arch Linux image.
+ copy_common_files
+ copy_file /etc/hosts.equiv
+ copy_file /etc/netgroup
+ copy_file /etc/networks
+ # No need for localtime as it is setup during the image build
+ #copy_file /etc/localtime
+ copy_passwd_and_group
+ fi
+
+ provide_common_bindings
+ local bindings=${RESULT}
+ unset RESULT
+
+ _run_env_with_qemu "$backend_command" "${bindings} -r ${JUNEST_HOME} $backend_args" "$@"
+}
diff --git a/lib/core/setup.sh b/lib/core/setup.sh
new file mode 100644
index 0000000..58c6122
--- /dev/null
+++ b/lib/core/setup.sh
@@ -0,0 +1,164 @@
+#!/usr/bin/env bash
+#
+# This module contains all setup functionalities for JuNest.
+#
+# Dependencies:
+# - lib/utils/utils.sh
+# - lib/core/common.sh
+#
+# vim: ft=sh
+
+#######################################
+# Check if the JuNest system is installed in JUNEST_HOME.
+#
+# Globals:
+# JUNEST_HOME (RO) : Contains the JuNest home directory.
+# Arguments:
+# None
+# Returns:
+# 0 : If JuNest is installed
+# 1 : If JuNest is not installed
+# Output:
+# None
+#######################################
+function is_env_installed(){
+ [[ -d "$JUNEST_HOME" ]] && [[ "$(ls -A "$JUNEST_HOME")" ]] && return 0
+ return 1
+}
+
+
+function _cleanup_build_directory(){
+ local maindir=$1
+ check_not_null "$maindir"
+ builtin cd "$ORIGIN_WD" || return 1
+ trap - QUIT EXIT ABRT KILL TERM INT
+ rm_cmd -fr "$maindir"
+}
+
+
+function _prepare_build_directory(){
+ local maindir=$1
+ check_not_null "$maindir"
+ trap - QUIT EXIT ABRT KILL TERM INT
+ # shellcheck disable=SC2064
+ trap "rm_cmd -rf ${maindir}; die \"Error occurred when installing ${NAME}\"" EXIT QUIT ABRT TERM INT
+}
+
+
+function _setup_env(){
+ local imagepath=$1
+ check_not_null "$imagepath"
+
+ is_env_installed && die "Error: ${NAME} has been already installed in $JUNEST_HOME"
+
+ mkdir_cmd -p "${JUNEST_HOME}"
+ $TAR -zxpf "${imagepath}" -C "${JUNEST_HOME}"
+ info "${NAME} installed successfully!"
+ echo
+ info "Default mirror URL set to: ${DEFAULT_MIRROR}"
+ info "You can change the pacman mirror URL in /etc/pacman.d/mirrorlist according to your location:"
+ info " \$EDITOR ${JUNEST_HOME}/etc/pacman.d/mirrorlist"
+ echo
+ info "Remember to refresh the package databases from the server:"
+ info " pacman -Syy"
+ echo
+ info "To install packages from AUR follow the wiki here:"
+ info "https://github.com/fsquillace/junest#install-packages-from-aur"
+}
+
+
+#######################################
+# Setup JuNest.
+#
+# Globals:
+# JUNEST_HOME (RO) : The JuNest home directory in which JuNest needs
+# to be installed.
+# ARCH (RO) : The host architecture.
+# JUNEST_TEMPDIR (RO) : The JuNest temporary directory for building
+# the JuNest system from the image.
+# ENV_REPO (RO) : URL of the site containing JuNest images.
+# NAME (RO) : The JuNest name.
+# Arguments:
+# arch ($1?) : The JuNest architecture image to download.
+# Defaults to the host architecture
+# Returns:
+# $NOT_AVAILABLE_ARCH : If the architecture is not one of the available ones.
+# Output:
+# None
+#######################################
+function setup_env(){
+ local arch=${1:-$ARCH}
+ contains_element "$arch" "${ARCH_LIST[@]}" || \
+ die_on_status "$NOT_AVAILABLE_ARCH" "The architecture is not one of: ${ARCH_LIST[*]}"
+
+ local maindir
+ maindir=$(TMPDIR=$JUNEST_TEMPDIR mktemp -d -t "${CMD}".XXXXXXXXXX)
+ _prepare_build_directory "$maindir"
+
+ info "Downloading ${NAME}..."
+ builtin cd "${maindir}" || return 1
+ local imagefile=${CMD}-${arch}.tar.gz
+ download_cmd "${ENV_REPO}/${imagefile}"
+
+ info "Installing ${NAME}..."
+ _setup_env "${maindir}/${imagefile}"
+
+ _cleanup_build_directory "${maindir}"
+}
+
+#######################################
+# Setup JuNest from file.
+#
+# Globals:
+# JUNEST_HOME (RO) : The JuNest home directory in which JuNest needs
+# to be installed.
+# NAME (RO) : The JuNest name.
+# Arguments:
+# imagefile ($1) : The JuNest image file.
+# Returns:
+# $NOT_EXISTING_FILE : If the image file does not exist.
+# Output:
+# None
+#######################################
+function setup_env_from_file(){
+ local imagefile=$1
+ check_not_null "$imagefile"
+ [[ ! -e ${imagefile} ]] && die_on_status "$NOT_EXISTING_FILE" "Error: The ${NAME} image file ${imagefile} does not exist"
+
+ info "Installing ${NAME} from ${imagefile}..."
+ _setup_env "${imagefile}"
+}
+
+#######################################
+# Remove an existing JuNest system.
+#
+# Globals:
+# JUNEST_HOME (RO) : The JuNest home directory to remove.
+# Arguments:
+# None
+# Returns:
+# None
+# Output:
+# None
+#######################################
+function delete_env(){
+ ! ask "Are you sure to delete ${NAME} located in ${JUNEST_HOME}" "N" && return
+ if mountpoint -q "${JUNEST_HOME}"
+ then
+ info "There are mounted directories inside ${JUNEST_HOME}"
+ if ! umount --force "${JUNEST_HOME}"
+ then
+ error "Cannot umount directories in ${JUNEST_HOME}"
+ die "Try to delete ${NAME} using root permissions"
+ fi
+ fi
+ # the CA directories are read only and can be deleted only by changing the mod
+ chmod -R +w "${JUNEST_HOME}"/etc/ca-certificates
+ if rm_cmd -rf "${JUNEST_HOME}"
+ then
+ info "${NAME} deleted in ${JUNEST_HOME}"
+ else
+ error "Error: Cannot delete ${NAME} in ${JUNEST_HOME}"
+ fi
+}
+
diff --git a/lib/core/wrappers.sh b/lib/core/wrappers.sh
new file mode 100644
index 0000000..1fe955c
--- /dev/null
+++ b/lib/core/wrappers.sh
@@ -0,0 +1,60 @@
+#!/usr/bin/env bash
+#
+# Dependencies:
+# None
+#
+# vim: ft=sh
+
+#######################################
+# Create bin wrappers
+#
+# Globals:
+# JUNEST_HOME (RO) : The JuNest home directory.
+# Arguments:
+# force ($1?) : Create bin wrappers even if the bin file exists.
+# Defaults to false.
+# Returns:
+# None
+# Output:
+# None
+#######################################
+function create_wrappers() {
+ local force=${1:-false}
+ local bin_path=${2:-/usr/bin}
+ bin_path=${bin_path%/}
+ mkdir -p "${JUNEST_HOME}${bin_path}_wrappers"
+ # Arguments inside a variable (i.e. `JUNEST_ARGS`) separated by quotes
+ # are not recognized normally unless using `eval`. More info here:
+ # https://github.com/fsquillace/junest/issues/262
+ # https://github.com/fsquillace/junest/pull/287
+ cat < "${JUNEST_HOME}/usr/bin/junest_wrapper"
+#!/usr/bin/env bash
+
+eval "junest_args_array=(\${JUNEST_ARGS:-ns})"
+junest "\${junest_args_array[@]}" -- \$(basename \${0}) "\$@"
+EOF
+ chmod +x "${JUNEST_HOME}/usr/bin/junest_wrapper"
+
+ cd "${JUNEST_HOME}${bin_path}" || return 1
+ for file in *
+ do
+ [[ -d $file ]] && continue
+ # Symlinks outside junest appear as broken even though they are correct
+ # within a junest session. The following do not skip broken symlinks:
+ [[ -x $file || -L $file ]] || continue
+ if [[ -e ${JUNEST_HOME}${bin_path}_wrappers/$file ]] && ! $force
+ then
+ continue
+ fi
+ rm -f "${JUNEST_HOME}${bin_path}_wrappers/$file"
+ ln -s "${JUNEST_HOME}/usr/bin/junest_wrapper" "${JUNEST_HOME}${bin_path}_wrappers/$file"
+ done
+
+ # Remove wrappers no longer needed
+ cd "${JUNEST_HOME}${bin_path}_wrappers" || return 1
+ for file in *
+ do
+ [[ -e ${JUNEST_HOME}${bin_path}/$file || -L ${JUNEST_HOME}${bin_path}/$file ]] || rm -f "$file"
+ done
+
+}
diff --git a/lib/util.sh b/lib/util.sh
deleted file mode 100644
index 15fd48c..0000000
--- a/lib/util.sh
+++ /dev/null
@@ -1,67 +0,0 @@
-#!/usr/bin/env bash
-#
-# This file is part of JuJu: The portable GNU/Linux distribution
-#
-# Copyright (c) 2012-2014 Filippo Squillace
-#
-# This program is free software; you can redistribute it and/or modify it
-# under the terms of the GNU Library General Public License as published
-# by the Free Software Foundation; either version 2, or (at your option)
-# any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program. If not, see .
-
-echoerr() { echo "$@" 1>&2; }
-function die(){
-# $@: msg (mandatory) - str: Message to print
- error $@
- exit 1
-}
-function error(){
-# $@: msg (mandatory) - str: Message to print
- echoerr -e "\033[1;31m$@\033[0m"
-}
-function warn(){
-# $@: msg (mandatory) - str: Message to print
- echoerr -e "\033[1;33m$@\033[0m"
-}
-function info(){
-# $@: msg (mandatory) - str: Message to print
- echo -e "\033[1;37m$@\033[0m"
-}
-
-function ask(){
- # $1: question string
- # $2: default value - can be either Y, y, N, n (by default Y)
-
- local default="Y"
- [ -z $2 ] || default=$(echo "$2" | tr '[:lower:]' '[:upper:]')
-
- local other="n"
- [ "$default" == "N" ] && other="y"
-
- local prompt="$1 (${default}/${other})> "
-
- local res="none"
- while [ "$res" != "Y" ] && [ "$res" != "N" ] && [ "$res" != "" ];
- do
- read -p "$prompt" res
- res=$(echo "$res" | tr '[:lower:]' '[:upper:]')
- done
-
- [ "$res" == "" ] && res="$default"
-
- if [ "$res" == "Y" ]
- then
- return 0
- else
- return 1
- fi
-
-}
diff --git a/lib/utils/utils.sh b/lib/utils/utils.sh
new file mode 100644
index 0000000..5659568
--- /dev/null
+++ b/lib/utils/utils.sh
@@ -0,0 +1,205 @@
+#!/usr/bin/env bash
+
+NULL_EXCEPTION=11
+WRONG_ANSWER=33
+
+#######################################
+# Check if the argument is null.
+#
+# Globals:
+# None
+# Arguments:
+# argument ($1) : Argument to check.
+# Returns:
+# 0 : If argument is not null.
+# NULL_EXCEPTION : If argument is null.
+# Output:
+# None
+#######################################
+function check_not_null() {
+ [ -z "$1" ] && { error "Error: null argument $1"; return $NULL_EXCEPTION; }
+ return 0
+}
+
+#######################################
+# Redirect message to stderr.
+#
+# Globals:
+# None
+# Arguments:
+# msg ($@): Message to print.
+# Returns:
+# None
+# Output:
+# Message printed to stderr.
+#######################################
+function echoerr() {
+ echo "$@" 1>&2;
+}
+
+#######################################
+# Print an error message to stderr and exit program.
+#
+# Globals:
+# None
+# Arguments:
+# msg ($@) : Message to print.
+# Returns:
+# 1 : The unique exit status printed.
+# Output:
+# Message printed to stderr.
+#######################################
+function die() {
+ error "$@"
+ exit 1
+}
+
+#######################################
+# Print an error message to stderr and exit program with a given status.
+#
+# Globals:
+# None
+# Arguments:
+# status ($1) : The exit status to use.
+# msg ($2-) : Message to print.
+# Returns:
+# $? : The $status exit status.
+# Output:
+# Message printed to stderr.
+#######################################
+function die_on_status() {
+ status=$1
+ shift
+ error "$@"
+ exit "$status"
+}
+
+#######################################
+# Print an error message to stderr.
+#
+# Globals:
+# None
+# Arguments:
+# msg ($@): Message to print.
+# Returns:
+# None
+# Output:
+# Message printed to stderr.
+#######################################
+function error() {
+ echoerr -e "\033[1;31m$*\033[0m"
+}
+
+#######################################
+# Print a warn message to stderr.
+#
+# Globals:
+# None
+# Arguments:
+# msg ($@): Message to print.
+# Returns:
+# None
+# Output:
+# Message printed to stderr.
+#######################################
+function warn() {
+ # $@: msg (mandatory) - str: Message to print
+ echoerr -e "\033[1;33m$*\033[0m"
+}
+
+#######################################
+# Print an info message to stdout.
+#
+# Globals:
+# None
+# Arguments:
+# msg ($@): Message to print.
+# Returns:
+# None
+# Output:
+# Message printed to stdout.
+#######################################
+function info(){
+ echo -e "\033[1;36m$*\033[0m"
+}
+
+#######################################
+# Ask a question and wait to receive an answer from stdin.
+# It returns $default_answer if no answer has be received from stdin.
+#
+# Globals:
+# None
+# Arguments:
+# question ($1) : The question to ask.
+# default_answer ($2) : Possible values: 'Y', 'y', 'N', 'n' (default: 'Y')
+# Returns:
+# 0 : If user replied with either 'Y' or 'y'.
+# 1 : If user replied with either 'N' or 'n'.
+# WRONG_ANSWER : If `default_answer` is not one of the possible values.
+# Output:
+# Print the question to ask.
+#######################################
+function ask(){
+ local question=$1
+ local default_answer=$2
+ check_not_null "$question"
+
+ if [ -n "$default_answer" ]
+ then
+ local answers="Y y N n"
+ [[ "$answers" =~ $default_answer ]] || { error "The default answer: $default_answer is wrong."; return $WRONG_ANSWER; }
+ fi
+
+ local default="Y"
+ [ -z "$default_answer" ] || default=$(echo "$default_answer" | tr '[:lower:]' '[:upper:]')
+
+ local other="n"
+ [ "$default" == "N" ] && other="y"
+
+ local prompt
+ prompt=$(info "$question (${default}/${other})> ")
+
+ local res="none"
+ while [ "$res" != "Y" ] && [ "$res" != "N" ] && [ "$res" != "" ];
+ do
+ read -r -p "$prompt" res
+ res=$(echo "$res" | tr '[:lower:]' '[:upper:]')
+ done
+
+ [ "$res" == "" ] && res="$default"
+
+ [ "$res" == "Y" ]
+}
+
+function insert_quotes_on_spaces(){
+# It inserts quotes between arguments.
+# Useful to preserve quotes on command
+# to be used inside sh -c/bash -c
+ local C=""
+ whitespace="[[:space:]]"
+ for i in "$@"
+ do
+ if [[ $i =~ $whitespace ]]
+ then
+ temp_C="\"$i\""
+ else
+ temp_C="$i"
+ fi
+
+ # Handle edge case when C is empty to avoid adding an extra space
+ if [[ -z $C ]]
+ then
+ C="$temp_C"
+ else
+ C="$C $temp_C"
+ fi
+
+ done
+ echo "$C"
+}
+
+contains_element () {
+ local e
+ for e in "${@:2}"; do [[ "$e" == "$1" ]] && return 0; done
+ return 1
+}
diff --git a/tests/checkstyle/checkstyle.sh b/tests/checkstyle/checkstyle.sh
new file mode 100755
index 0000000..4f71965
--- /dev/null
+++ b/tests/checkstyle/checkstyle.sh
@@ -0,0 +1,22 @@
+#!/usr/bin/env bash
+# shellcheck disable=SC1091
+
+source "$(dirname "$0")/../utils/utils.sh"
+
+# Disable the exiterr
+set +e
+
+function oneTimeSetUp(){
+ setUpUnitTests
+}
+
+function test_check_no_tabs(){
+ assertCommandFailOnStatus 1 grep -R "$(printf '\t')" "$(dirname "$0")"/../../bin/*
+ assertEquals "" "$(cat "$STDOUTF")"
+ assertEquals "" "$(cat "$STDERRF")"
+ assertCommandFailOnStatus 1 grep -R "$(printf '\t')" "$(dirname "$0")"/../../lib/*
+ assertEquals "" "$(cat "$STDOUTF")"
+ assertEquals "" "$(cat "$STDERRF")"
+}
+
+source "$(dirname "$0")"/../utils/shunit2
diff --git a/tests/unit-tests/test-chroot.sh b/tests/unit-tests/test-chroot.sh
new file mode 100755
index 0000000..3739c58
--- /dev/null
+++ b/tests/unit-tests/test-chroot.sh
@@ -0,0 +1,125 @@
+#!/bin/bash
+# shellcheck disable=SC1091
+
+JUNEST_ROOT=$(readlink -f "$(dirname "$0")"/../..)
+
+source "$JUNEST_ROOT/tests/utils/utils.sh"
+
+source "$JUNEST_ROOT/lib/utils/utils.sh"
+source "$JUNEST_ROOT/lib/core/common.sh"
+source "$JUNEST_ROOT/lib/core/chroot.sh"
+
+# Disable the exiterr
+set +e
+
+function oneTimeSetUp(){
+ setUpUnitTests
+}
+
+function setUp(){
+ cwdSetUp
+ junestSetUp
+ init_mocks
+}
+
+function tearDown(){
+ junestTearDown
+ cwdTearDown
+}
+
+function init_mocks() {
+ chroot_cmd() {
+ # shellcheck disable=SC2317
+ [ "$JUNEST_ENV" != "1" ] && return 1
+ # shellcheck disable=SC2317
+ echo "chroot_cmd $*"
+ }
+ # shellcheck disable=SC2034
+ GROOT=chroot_cmd
+ mychroot() {
+ # shellcheck disable=SC2317
+ echo mychroot "$*"
+ }
+}
+
+function test_run_env_as_groot_cmd(){
+ assertCommandSuccess run_env_as_groot "" "" "false" pwd
+ assertEquals "chroot_cmd -b /run/user/$(id -u) -b $HOME -b /tmp -b /proc -b /sys -b /dev $JUNEST_HOME /bin/sh --login -c pwd" "$(cat "$STDOUTF")"
+}
+
+function test_run_env_as_groot_no_cmd(){
+ assertCommandSuccess run_env_as_groot "" "" "false" ""
+ assertEquals "chroot_cmd -b /run/user/$(id -u) -b $HOME -b /tmp -b /proc -b /sys -b /dev $JUNEST_HOME /bin/sh --login" "$(cat "$STDOUTF")"
+}
+
+function test_run_env_as_groot_with_backend_command(){
+ assertCommandSuccess run_env_as_groot "mychroot" "" "false" ""
+ assertEquals "mychroot -b /run/user/$(id -u) -b $HOME -b /tmp -b /proc -b /sys -b /dev $JUNEST_HOME /bin/sh --login" "$(cat "$STDOUTF")"
+}
+
+function test_run_env_as_groot_no_copy(){
+ assertCommandSuccess run_env_as_groot "" "" "true" pwd
+ assertEquals "chroot_cmd -b /run/user/$(id -u) -b $HOME -b /tmp -b /proc -b /sys -b /dev $JUNEST_HOME /bin/sh --login -c pwd" "$(cat "$STDOUTF")"
+
+ [[ ! -e ${JUNEST_HOME}/etc/hosts ]]
+ assertEquals 0 $?
+ [[ ! -e ${JUNEST_HOME}/etc/host.conf ]]
+ assertEquals 0 $?
+ [[ ! -e ${JUNEST_HOME}/etc/nsswitch.conf ]]
+ assertEquals 0 $?
+ [[ ! -e ${JUNEST_HOME}/etc/resolv.conf ]]
+ assertEquals 0 $?
+}
+
+function test_run_env_as_groot_nested_env(){
+ JUNEST_ENV=1
+ assertCommandFailOnStatus 106 run_env_as_groot "" "" "false" ""
+ unset JUNEST_ENV
+}
+
+function test_run_env_as_groot_cmd_with_backend_args(){
+ assertCommandSuccess run_env_as_groot "" "-n -b /home/blah" "false" pwd
+ assertEquals "chroot_cmd -b /run/user/$(id -u) -b $HOME -b /tmp -b /proc -b /sys -b /dev -n -b /home/blah $JUNEST_HOME /bin/sh --login -c pwd" "$(cat "$STDOUTF")"
+}
+
+function test_run_env_as_chroot_cmd(){
+ assertCommandSuccess run_env_as_chroot "" "" "false" pwd
+ assertEquals "chroot_cmd $JUNEST_HOME /bin/sh --login -c pwd" "$(cat "$STDOUTF")"
+}
+
+function test_run_env_as_chroot_no_cmd(){
+ assertCommandSuccess run_env_as_chroot "" "" "false" ""
+ assertEquals "chroot_cmd $JUNEST_HOME /bin/sh --login" "$(cat "$STDOUTF")"
+}
+
+function test_run_env_as_chroot_with_backend_command(){
+ assertCommandSuccess run_env_as_chroot "mychroot" "" "false" ""
+ assertEquals "mychroot $JUNEST_HOME /bin/sh --login" "$(cat "$STDOUTF")"
+}
+
+function test_run_env_as_chroot_no_copy(){
+ assertCommandSuccess run_env_as_chroot "" "" "true" pwd
+ assertEquals "chroot_cmd $JUNEST_HOME /bin/sh --login -c pwd" "$(cat "$STDOUTF")"
+
+ [[ ! -e ${JUNEST_HOME}/etc/hosts ]]
+ assertEquals 0 $?
+ [[ ! -e ${JUNEST_HOME}/etc/host.conf ]]
+ assertEquals 0 $?
+ [[ ! -e ${JUNEST_HOME}/etc/nsswitch.conf ]]
+ assertEquals 0 $?
+ [[ ! -e ${JUNEST_HOME}/etc/resolv.conf ]]
+ assertEquals 0 $?
+}
+
+function test_run_env_as_choot_nested_env(){
+ JUNEST_ENV=1
+ assertCommandFailOnStatus 106 run_env_as_chroot "" "" "false" ""
+ unset JUNEST_ENV
+}
+
+function test_run_env_as_chroot_cmd_with_backend_args(){
+ assertCommandSuccess run_env_as_chroot "" "-n -b /home/blah" "false" pwd
+ assertEquals "chroot_cmd -n -b /home/blah $JUNEST_HOME /bin/sh --login -c pwd" "$(cat "$STDOUTF")"
+}
+
+source "$JUNEST_ROOT"/tests/utils/shunit2
diff --git a/tests/unit-tests/test-common.sh b/tests/unit-tests/test-common.sh
new file mode 100755
index 0000000..89c1a1e
--- /dev/null
+++ b/tests/unit-tests/test-common.sh
@@ -0,0 +1,242 @@
+#!/bin/bash
+# shellcheck disable=SC1091
+
+JUNEST_ROOT=$(readlink -f "$(dirname "$0")"/../..)
+
+source "$JUNEST_ROOT/tests/utils/utils.sh"
+
+source "$JUNEST_ROOT/lib/utils/utils.sh"
+source "$JUNEST_ROOT/lib/core/common.sh"
+
+# Disable the exiterr
+set +e
+
+function oneTimeSetUp(){
+ setUpUnitTests
+ junestSetUp
+}
+
+function oneTimeTearDown(){
+ junestTearDown
+}
+
+function setUp(){
+ ld_exec_mock() {
+ # shellcheck disable=SC2317
+ echo "ld_exec $*"
+ }
+ # shellcheck disable=SC2317
+ ld_exec_mock_false() {
+ echo "ld_exec $*"
+ return 1
+ }
+ # shellcheck disable=SC2034
+ LD_EXEC=ld_exec_mock
+
+ unshare_mock() {
+ # shellcheck disable=SC2317
+ echo "unshare $*"
+ }
+ # shellcheck disable=SC2034
+ UNSHARE=unshare_mock
+
+ # shellcheck disable=SC2317
+ bwrap_mock() {
+ echo "bwrap $*"
+ }
+ # shellcheck disable=SC2034
+ BWRAP=bwrap_mock
+
+}
+
+function test_ln(){
+ LN="echo" assertCommandSuccess ln_cmd -s ln_file new_file
+ assertEquals "-s ln_file new_file" "$(cat "$STDOUTF")"
+
+ LN=false assertCommandSuccess ln_cmd -s ln_file new_file
+ assertEquals "ld_exec ${JUNEST_HOME}/usr/bin/false -s ln_file new_file" "$(cat "$STDOUTF")"
+
+ LN=false LD_EXEC=false assertCommandFail ln_cmd
+}
+
+function test_getent(){
+ GETENT="echo" assertCommandSuccess getent_cmd passwd
+ assertEquals "passwd" "$(cat "$STDOUTF")"
+
+ GETENT="false" assertCommandSuccess getent_cmd passwd
+ assertEquals "ld_exec ${JUNEST_HOME}/usr/bin/false passwd" "$(cat "$STDOUTF")"
+
+ GETENT=false LD_EXEC=false assertCommandFail getent_cmd
+}
+
+function test_cp(){
+ CP="echo" assertCommandSuccess cp_cmd passwd
+ assertEquals "passwd" "$(cat "$STDOUTF")"
+
+ CP=false assertCommandSuccess cp_cmd passwd
+ assertEquals "ld_exec ${JUNEST_HOME}/usr/bin/false passwd" "$(cat "$STDOUTF")"
+
+ CP=false LD_EXEC=false assertCommandFail cp_cmd
+}
+
+function test_download(){
+ WGET=/bin/true
+ CURL=/bin/false
+ assertCommandSuccess download_cmd
+
+ # shellcheck disable=SC2034
+ WGET=/bin/false
+ # shellcheck disable=SC2034
+ CURL=/bin/true
+ assertCommandSuccess download_cmd
+
+ WGET=/bin/false CURL=/bin/false assertCommandFail download_cmd
+}
+
+function test_rm(){
+ RM="echo" assertCommandSuccess rm_cmd rm_file
+ assertEquals "rm_file" "$(cat "$STDOUTF")"
+
+ RM="false" assertCommandSuccess rm_cmd rm_file
+ assertEquals "ld_exec ${JUNEST_HOME}/usr/bin/false rm_file" "$(cat "$STDOUTF")"
+
+ RM=false LD_EXEC=false assertCommandFail rm_cmd rm_file
+}
+
+function test_chown(){
+ local id
+ id=$(id -u)
+
+ CHOWN="echo" assertCommandSuccess chown_cmd "$id" chown_file
+ assertEquals "$id chown_file" "$(cat "$STDOUTF")"
+
+ CHOWN="false" assertCommandSuccess chown_cmd "$id" chown_file
+ assertEquals "ld_exec ${JUNEST_HOME}/usr/bin/false $id chown_file" "$(cat "$STDOUTF")"
+
+ CHOWN=false LD_EXEC=false assertCommandFail chown_cmd "$id" chown_file
+}
+
+function test_mkdir(){
+ MKDIR="echo" assertCommandSuccess mkdir_cmd -p new_dir/new_dir
+ assertEquals "-p new_dir/new_dir" "$(cat "$STDOUTF")"
+
+ MKDIR=false assertCommandSuccess mkdir_cmd -p new_dir/new_dir
+ assertEquals "ld_exec ${JUNEST_HOME}/usr/bin/false -p new_dir/new_dir" "$(cat "$STDOUTF")"
+
+ MKDIR=false LD_EXEC=false assertCommandFail mkdir_cmd -p new_dir/new_dir
+}
+
+function test_zgrep(){
+ ZGREP="echo" assertCommandSuccess zgrep_cmd new_file
+ assertEquals "new_file" "$(cat "$STDOUTF")"
+
+ mkdir -p "${JUNEST_HOME}"/usr/bin
+ touch "${JUNEST_HOME}"/usr/bin/false
+ chmod +x "${JUNEST_HOME}"/usr/bin/false
+
+ echo -e "#!/bin/bash\necho zgrep" > "${JUNEST_HOME}"/usr/bin/false
+ ZGREP=false assertCommandSuccess zgrep_cmd new_file
+ assertEquals "zgrep" "$(cat "$STDOUTF")"
+
+ echo -e "#!/bin/bash\nexit 1" > "${JUNEST_HOME}"/usr/bin/false
+ ZGREP=false assertCommandFail zgrep_cmd new_file
+}
+
+function test_unshare(){
+ assertCommandSuccess unshare_cmd new_program
+ assertEquals "$(echo -e "ld_exec ${JUNEST_HOME}/usr/bin/$UNSHARE --user /bin/sh -c :\nld_exec ${JUNEST_HOME}/usr/bin/$UNSHARE new_program")" "$(cat "$STDOUTF")"
+
+ LD_EXEC=ld_exec_mock_false assertCommandSuccess unshare_cmd new_program
+ assertEquals "$(echo -e "ld_exec ${JUNEST_HOME}/usr/bin/unshare_mock --user /bin/sh -c :\nunshare --user /bin/sh -c :\nunshare new_program")" "$(cat "$STDOUTF")"
+
+ UNSHARE=false LD_EXEC=false assertCommandFail unshare_cmd new_program
+}
+
+function test_bwrap(){
+ assertCommandSuccess bwrap_cmd new_program
+ assertEquals "$(echo -e "ld_exec $BWRAP --dev-bind / / /bin/sh -c :\nld_exec $BWRAP new_program")" "$(cat "$STDOUTF")"
+
+ BWRAP=false LD_EXEC=false assertCommandFail bwrap_cmd new_program
+}
+
+function test_chroot(){
+ CLASSIC_CHROOT="echo" assertCommandSuccess chroot_cmd root
+ assertEquals "root" "$(cat "$STDOUTF")"
+
+ CLASSIC_CHROOT=false assertCommandSuccess chroot_cmd root
+ assertEquals "ld_exec $JUNEST_HOME/usr/bin/false root" "$(cat "$STDOUTF")"
+
+ CLASSIC_CHROOT=false LD_EXEC=false assertCommandFail chroot_cmd root
+}
+
+function test_proot_cmd_compat(){
+ PROOT="/bin/true" assertCommandSuccess proot_cmd "" ""
+
+ PROOT="/bin/false" assertCommandFail proot_cmd --helps
+}
+
+function test_proot_cmd_seccomp(){
+ envv(){
+ # shellcheck disable=SC2317
+ env
+ }
+ PROOT=envv
+ assertCommandSuccess proot_cmd cmd
+ assertEquals "" "$(grep "^PROOT_NO_SECCOMP" "$STDOUTF")"
+
+ envv(){
+ # shellcheck disable=SC2317
+ env | grep "^PROOT_NO_SECCOMP"
+ }
+ # shellcheck disable=SC2034
+ PROOT=envv
+ assertCommandSuccess proot_cmd cmd
+ # The variable PROOT_NO_SECCOMP will be produced
+ # twice due to the fallback mechanism
+ assertEquals "PROOT_NO_SECCOMP=1
+PROOT_NO_SECCOMP=1" "$(grep "^PROOT_NO_SECCOMP" "$STDOUTF")"
+}
+
+function test_copy_passwd_and_group(){
+ getent_cmd_mock() {
+ # shellcheck disable=SC2317
+ echo "$*"
+ }
+ GETENT=getent_cmd_mock assertCommandSuccess copy_passwd_and_group
+ assertEquals "$(echo -e "passwd\npasswd $USER")" "$(cat "$JUNEST_HOME"/etc/passwd)"
+ assertEquals "group" "$(cat "$JUNEST_HOME"/etc/group)"
+}
+
+function test_copy_passwd_and_group_fallback(){
+ cp_cmd_mock() {
+ # shellcheck disable=SC2317
+ echo "$*"
+ }
+ CP=cp_cmd_mock GETENT=false LD_EXEC=false assertCommandSuccess copy_passwd_and_group
+ assertEquals "$(echo -e "-f /etc/passwd $JUNEST_HOME//etc/passwd\n-f /etc/group $JUNEST_HOME//etc/group")" "$(cat "$STDOUTF")"
+}
+
+function test_copy_passwd_and_group_failure(){
+ CP=false GETENT=false LD_EXEC=false assertCommandFailOnStatus 1 copy_passwd_and_group
+}
+
+function test_nested_env(){
+ JUNEST_ENV=1 assertCommandFailOnStatus 106 check_nested_env
+}
+
+function test_nested_env_not_set_variable(){
+ JUNEST_ENV=aaa assertCommandFailOnStatus 107 check_nested_env
+}
+
+function test_check_same_arch_not_same(){
+ echo "JUNEST_ARCH=XXX" > "${JUNEST_HOME}"/etc/junest/info
+ assertCommandFailOnStatus 104 check_same_arch
+}
+
+function test_check_same_arch(){
+ echo "JUNEST_ARCH=$ARCH" > "${JUNEST_HOME}"/etc/junest/info
+ assertCommandSuccess check_same_arch
+}
+
+
+source "$JUNEST_ROOT"/tests/utils/shunit2
diff --git a/tests/unit-tests/test-junest.sh b/tests/unit-tests/test-junest.sh
new file mode 100755
index 0000000..07a92b1
--- /dev/null
+++ b/tests/unit-tests/test-junest.sh
@@ -0,0 +1,383 @@
+#!/bin/bash
+# shellcheck disable=SC1091
+
+source "$(dirname "$0")/../utils/utils.sh"
+
+JUNEST_BASE="$(dirname "$0")/../.."
+source "$JUNEST_BASE"/bin/junest -h &> /dev/null
+
+# Disable the exiterr
+set +e
+
+function oneTimeSetUp(){
+ setUpUnitTests
+}
+
+function setUp(){
+ ## Mock functions ##
+ # shellcheck disable=SC2317
+ function usage(){
+ echo "usage"
+ }
+ # shellcheck disable=SC2317
+ function version(){
+ echo "version"
+ }
+ # shellcheck disable=SC2317
+ function build_image_env(){
+ local disable_check=$1
+ echo "build_image_env($disable_check)"
+ }
+ # shellcheck disable=SC2317
+ function delete_env(){
+ echo "delete_env"
+ }
+ # shellcheck disable=SC2317
+ function setup_env_from_file(){
+ echo "setup_env_from_file($1)"
+ }
+ # shellcheck disable=SC2317
+ function setup_env(){
+ echo "setup_env($1)"
+ }
+ # shellcheck disable=SC2317
+ function run_env_as_proot_fakeroot(){
+ local backend_command="$1"
+ local backend_args="$2"
+ local no_copy_files="$3"
+ shift 3
+ echo "run_env_as_proot_fakeroot($backend_command,$backend_args,$no_copy_files,$*)"
+ }
+ # shellcheck disable=SC2317
+ function run_env_as_groot(){
+ local backend_command="$1"
+ local backend_args="$2"
+ local no_copy_files="$3"
+ shift 3
+ echo "run_env_as_groot($backend_command,$backend_args,$no_copy_files,$*)"
+ }
+ # shellcheck disable=SC2317
+ function run_env_as_chroot(){
+ local backend_command="$1"
+ local backend_args="$2"
+ local no_copy_files="$3"
+ shift 3
+ echo "run_env_as_chroot($backend_command,$backend_args,$no_copy_files,$*)"
+ }
+ # shellcheck disable=SC2317
+ function run_env_as_proot_user(){
+ local backend_command="$1"
+ local backend_args="$2"
+ local no_copy_files="$3"
+ shift 3
+ echo "run_env_as_proot_user($backend_command,$backend_args,$no_copy_files,$*)"
+ }
+ # shellcheck disable=SC2317
+ function run_env_as_bwrap_fakeroot(){
+ local backend_command="$1"
+ local backend_args="$2"
+ local no_copy_files="$3"
+ shift 3
+ echo "run_env_as_bwrap_fakeroot($backend_command,$backend_args,$no_copy_files,$*)"
+ }
+ # shellcheck disable=SC2317
+ function run_env_as_bwrap_user(){
+ local backend_command="$1"
+ local backend_args="$2"
+ local no_copy_files="$3"
+ shift 3
+ echo "run_env_as_bwrap_user($backend_command,$backend_args,$no_copy_files,$*)"
+ }
+ # shellcheck disable=SC2317
+ function is_env_installed(){
+ return 0
+ }
+ # shellcheck disable=SC2317
+ function create_wrappers(){
+ :
+ }
+}
+
+function test_help(){
+ assertCommandSuccess main -h
+ assertEquals "usage" "$(cat "$STDOUTF")"
+ assertCommandSuccess main --help
+ assertEquals "usage" "$(cat "$STDOUTF")"
+}
+function test_version(){
+ assertCommandSuccess main -V
+ assertEquals "version" "$(cat "$STDOUTF")"
+ assertCommandSuccess main --version
+ assertEquals "version" "$(cat "$STDOUTF")"
+}
+function test_build_image_env(){
+ assertCommandSuccess main b
+ assertEquals "build_image_env(false)" "$(cat "$STDOUTF")"
+ assertCommandSuccess main build
+ assertEquals "build_image_env(false)" "$(cat "$STDOUTF")"
+ assertCommandSuccess main b -n
+ assertEquals "build_image_env(true)" "$(cat "$STDOUTF")"
+ assertCommandSuccess main build --disable-check
+ assertEquals "build_image_env(true)" "$(cat "$STDOUTF")"
+}
+
+function test_create_wrappers(){
+ # shellcheck disable=SC2317
+ function create_wrappers(){
+ local force=$1
+ echo "create_wrappers($force)"
+ }
+ assertCommandSuccess main create-bin-wrappers
+ assertEquals "create_wrappers(false)" "$(cat "$STDOUTF")"
+
+ assertCommandSuccess main create-bin-wrappers --force
+ assertEquals "create_wrappers(true)" "$(cat "$STDOUTF")"
+}
+
+function test_delete_env(){
+ assertCommandSuccess main s -d
+ assertEquals "delete_env" "$(cat "$STDOUTF")"
+ assertCommandSuccess main setup --delete
+ assertEquals "delete_env" "$(cat "$STDOUTF")"
+}
+function test_setup_env_from_file(){
+ # shellcheck disable=SC2317
+ is_env_installed(){
+ return 1
+ }
+ assertCommandSuccess main s -i myimage
+ assertEquals "setup_env_from_file(myimage)" "$(cat "$STDOUTF")"
+ assertCommandSuccess main setup --from-file myimage
+ assertEquals "setup_env_from_file(myimage)" "$(cat "$STDOUTF")"
+
+ # shellcheck disable=SC2317
+ is_env_installed(){
+ return 0
+ }
+ assertCommandFail main setup -i myimage
+}
+
+function test_setup_env(){
+ # shellcheck disable=SC2317
+ is_env_installed(){
+ return 1
+ }
+ assertCommandSuccess main s
+ assertEquals "setup_env()" "$(cat "$STDOUTF")"
+ assertCommandSuccess main setup
+ assertEquals "setup_env()" "$(cat "$STDOUTF")"
+ assertCommandSuccess main s -a arm
+ assertEquals "setup_env(arm)" "$(cat "$STDOUTF")"
+ assertCommandSuccess main setup --arch arm
+ assertEquals "setup_env(arm)" "$(cat "$STDOUTF")"
+
+ # shellcheck disable=SC2317
+ is_env_installed(){
+ return 0
+ }
+ assertCommandFail main setup -a arm
+}
+
+function test_run_env_as_proot_fakeroot(){
+ assertCommandSuccess main p -f
+ assertEquals "run_env_as_proot_fakeroot(,,false,)" "$(cat "$STDOUTF")"
+ assertCommandSuccess main proot --fakeroot
+ assertEquals "run_env_as_proot_fakeroot(,,false,)" "$(cat "$STDOUTF")"
+ assertCommandSuccess main p -f -n
+ assertEquals "run_env_as_proot_fakeroot(,,true,)" "$(cat "$STDOUTF")"
+
+ assertCommandSuccess main p -f --backend-command blah
+ assertEquals "run_env_as_proot_fakeroot(blah,,false,)" "$(cat "$STDOUTF")"
+ assertCommandSuccess main proot -f --backend-command blah
+ assertEquals "run_env_as_proot_fakeroot(blah,,false,)" "$(cat "$STDOUTF")"
+
+ assertCommandSuccess main proot -f -b "-b arg"
+ assertEquals "run_env_as_proot_fakeroot(,-b arg,false,)" "$(cat "$STDOUTF")"
+ assertCommandSuccess main proot -f -b "-b arg" -- command -kv
+ assertEquals "run_env_as_proot_fakeroot(,-b arg,false,command -kv)" "$(cat "$STDOUTF")"
+ assertCommandSuccess main proot -f command --as
+ assertEquals "run_env_as_proot_fakeroot(,,false,command --as)" "$(cat "$STDOUTF")"
+ assertCommandSuccess main proot -f -- command --as
+ assertEquals "run_env_as_proot_fakeroot(,,false,command --as)" "$(cat "$STDOUTF")"
+
+ # shellcheck disable=SC2317
+ is_env_installed(){
+ return 1
+ }
+ assertCommandFail main proot -f
+}
+
+function test_run_env_as_user(){
+ assertCommandSuccess main proot
+ assertEquals "run_env_as_proot_user(,,false,)" "$(cat "$STDOUTF")"
+ assertCommandSuccess main p -n
+ assertEquals "run_env_as_proot_user(,,true,)" "$(cat "$STDOUTF")"
+
+ assertCommandSuccess main p --backend-command blah
+ assertEquals "run_env_as_proot_user(blah,,false,)" "$(cat "$STDOUTF")"
+ assertCommandSuccess main proot --backend-command blah
+ assertEquals "run_env_as_proot_user(blah,,false,)" "$(cat "$STDOUTF")"
+
+ assertCommandSuccess main proot -b "-b arg"
+ assertEquals "run_env_as_proot_user(,-b arg,false,)" "$(cat "$STDOUTF")"
+ assertCommandSuccess main proot -b "-b arg" -- command -ll
+ assertEquals "run_env_as_proot_user(,-b arg,false,command -ll)" "$(cat "$STDOUTF")"
+ assertCommandSuccess main proot command -ls
+ assertEquals "run_env_as_proot_user(,,false,command -ls)" "$(cat "$STDOUTF")"
+ assertCommandSuccess main proot -- command -ls
+ assertEquals "run_env_as_proot_user(,,false,command -ls)" "$(cat "$STDOUTF")"
+
+ # shellcheck disable=SC2317
+ is_env_installed(){
+ return 1
+ }
+ assertCommandFail main proot
+}
+
+function test_run_env_as_groot(){
+ assertCommandSuccess main g
+ assertEquals "run_env_as_groot(,,false,)" "$(cat "$STDOUTF")"
+ assertCommandSuccess main g -n
+ assertEquals "run_env_as_groot(,,true,)" "$(cat "$STDOUTF")"
+ assertCommandSuccess main g -b "-b arg"
+ assertEquals "run_env_as_groot(,-b arg,false,)" "$(cat "$STDOUTF")"
+
+ assertCommandSuccess main g --backend-command blah
+ assertEquals "run_env_as_groot(blah,,false,)" "$(cat "$STDOUTF")"
+ assertCommandSuccess main groot --backend-command blah
+ assertEquals "run_env_as_groot(blah,,false,)" "$(cat "$STDOUTF")"
+
+ assertCommandSuccess main groot command
+ assertEquals "run_env_as_groot(,,false,command)" "$(cat "$STDOUTF")"
+ assertCommandSuccess main groot -- command
+ assertEquals "run_env_as_groot(,,false,command)" "$(cat "$STDOUTF")"
+
+ # shellcheck disable=SC2317
+ is_env_installed(){
+ return 1
+ }
+ assertCommandFail main groot
+}
+
+function test_run_env_as_chroot(){
+ assertCommandSuccess main r
+ assertEquals "run_env_as_chroot(,,false,)" "$(cat "$STDOUTF")"
+ assertCommandSuccess main r -b "-b arg"
+ assertEquals "run_env_as_chroot(,-b arg,false,)" "$(cat "$STDOUTF")"
+
+ assertCommandSuccess main r --backend-command blah
+ assertEquals "run_env_as_chroot(blah,,false,)" "$(cat "$STDOUTF")"
+ assertCommandSuccess main root --backend-command blah
+ assertEquals "run_env_as_chroot(blah,,false,)" "$(cat "$STDOUTF")"
+
+ assertCommandSuccess main root command
+ assertEquals "run_env_as_chroot(,,false,command)" "$(cat "$STDOUTF")"
+ assertCommandSuccess main root -- command
+ assertEquals "run_env_as_chroot(,,false,command)" "$(cat "$STDOUTF")"
+
+ # shellcheck disable=SC2317
+ is_env_installed(){
+ return 1
+ }
+ assertCommandFail main root -f
+}
+
+function test_run_env_as_bwrap_fakeroot(){
+ assertCommandSuccess main n -f
+ assertEquals "run_env_as_bwrap_fakeroot(,,false,)" "$(cat "$STDOUTF")"
+ assertCommandSuccess main ns -f
+ assertEquals "run_env_as_bwrap_fakeroot(,,false,)" "$(cat "$STDOUTF")"
+ assertCommandSuccess main ns -n -f
+ assertEquals "run_env_as_bwrap_fakeroot(,,true,)" "$(cat "$STDOUTF")"
+
+ assertCommandSuccess main ns -f -b "-b arg"
+ assertEquals "run_env_as_bwrap_fakeroot(,-b arg,false,)" "$(cat "$STDOUTF")"
+ assertCommandSuccess main ns -f -b "-b arg" -- command -kv
+ assertEquals "run_env_as_bwrap_fakeroot(,-b arg,false,command -kv)" "$(cat "$STDOUTF")"
+ assertCommandSuccess main ns -f command --as
+ assertEquals "run_env_as_bwrap_fakeroot(,,false,command --as)" "$(cat "$STDOUTF")"
+ assertCommandSuccess main ns -f -- command --as
+ assertEquals "run_env_as_bwrap_fakeroot(,,false,command --as)" "$(cat "$STDOUTF")"
+
+ assertCommandSuccess main ns -f --backend-command blah
+ assertEquals "run_env_as_bwrap_fakeroot(blah,,false,)" "$(cat "$STDOUTF")"
+ assertCommandSuccess main -f --backend-command blah
+ assertEquals "run_env_as_bwrap_fakeroot(blah,,false,)" "$(cat "$STDOUTF")"
+
+ assertCommandSuccess main -f
+ assertEquals "run_env_as_bwrap_fakeroot(,,false,)" "$(cat "$STDOUTF")"
+ assertCommandSuccess main -f
+ assertEquals "run_env_as_bwrap_fakeroot(,,false,)" "$(cat "$STDOUTF")"
+
+ assertCommandSuccess main -f -b "-b arg"
+ assertEquals "run_env_as_bwrap_fakeroot(,-b arg,false,)" "$(cat "$STDOUTF")"
+ assertCommandSuccess main -f -b "-b arg" -- command -kv
+ assertEquals "run_env_as_bwrap_fakeroot(,-b arg,false,command -kv)" "$(cat "$STDOUTF")"
+ assertCommandSuccess main -f command --as
+ assertEquals "run_env_as_bwrap_fakeroot(,,false,command --as)" "$(cat "$STDOUTF")"
+ assertCommandSuccess main -f -- command --as
+ assertEquals "run_env_as_bwrap_fakeroot(,,false,command --as)" "$(cat "$STDOUTF")"
+
+ # shellcheck disable=SC2317
+ is_env_installed(){
+ return 1
+ }
+ assertCommandFail main ns -f
+}
+
+function test_run_env_as_bwrap_user(){
+ assertCommandSuccess main n
+ assertEquals "run_env_as_bwrap_user(,,false,)" "$(cat "$STDOUTF")"
+ assertCommandSuccess main ns
+ assertEquals "run_env_as_bwrap_user(,,false,)" "$(cat "$STDOUTF")"
+ assertCommandSuccess main ns -n
+ assertEquals "run_env_as_bwrap_user(,,true,)" "$(cat "$STDOUTF")"
+
+ assertCommandSuccess main ns -b "-b arg"
+ assertEquals "run_env_as_bwrap_user(,-b arg,false,)" "$(cat "$STDOUTF")"
+ assertCommandSuccess main ns -b "-b arg" -- command -kv
+ assertEquals "run_env_as_bwrap_user(,-b arg,false,command -kv)" "$(cat "$STDOUTF")"
+ assertCommandSuccess main ns command --as
+ assertEquals "run_env_as_bwrap_user(,,false,command --as)" "$(cat "$STDOUTF")"
+ assertCommandSuccess main ns -- command --as
+ assertEquals "run_env_as_bwrap_user(,,false,command --as)" "$(cat "$STDOUTF")"
+
+ assertCommandSuccess main ns --backend-command blah
+ assertEquals "run_env_as_bwrap_user(blah,,false,)" "$(cat "$STDOUTF")"
+ assertCommandSuccess main --backend-command blah
+ assertEquals "run_env_as_bwrap_user(blah,,false,)" "$(cat "$STDOUTF")"
+
+ assertCommandSuccess main
+ assertEquals "run_env_as_bwrap_user(,,false,)" "$(cat "$STDOUTF")"
+ assertCommandSuccess main
+ assertEquals "run_env_as_bwrap_user(,,false,)" "$(cat "$STDOUTF")"
+
+ assertCommandSuccess main -b "-b arg"
+ assertEquals "run_env_as_bwrap_user(,-b arg,false,)" "$(cat "$STDOUTF")"
+ assertCommandSuccess main -b "-b arg" -- command -kv
+ assertEquals "run_env_as_bwrap_user(,-b arg,false,command -kv)" "$(cat "$STDOUTF")"
+ assertCommandSuccess main command --as
+ assertEquals "run_env_as_bwrap_user(,,false,command --as)" "$(cat "$STDOUTF")"
+ assertCommandSuccess main -- command --as
+ assertEquals "run_env_as_bwrap_user(,,false,command --as)" "$(cat "$STDOUTF")"
+
+ # shellcheck disable=SC2317
+ is_env_installed(){
+ return 1
+ }
+ assertCommandFail main ns
+}
+
+function test_invalid_option(){
+ assertCommandFail main --no-option
+ assertCommandFail main n --no-option
+ assertCommandFail main g --no-option
+ assertCommandFail main r --no-option
+
+ assertCommandFail main p --no-option
+
+ assertCommandFail main b --no-option
+ assertCommandFail main s --no-option
+}
+
+source "$(dirname "$0")"/../utils/shunit2
diff --git a/tests/unit-tests/test-namespace.sh b/tests/unit-tests/test-namespace.sh
new file mode 100755
index 0000000..7a845aa
--- /dev/null
+++ b/tests/unit-tests/test-namespace.sh
@@ -0,0 +1,241 @@
+#!/bin/bash
+# shellcheck disable=SC1091
+
+JUNEST_ROOT=$(readlink -f "$(dirname "$0")"/../..)
+
+source "$JUNEST_ROOT/tests/utils/utils.sh"
+
+source "$JUNEST_ROOT/lib/utils/utils.sh"
+
+# Disable the exiterr
+set +e
+
+function oneTimeSetUp(){
+ setUpUnitTests
+}
+
+## Mock functions ##
+function init_mocks() {
+ # shellcheck disable=SC2317
+ function bwrap_cmd(){
+ echo "$BWRAP $*"
+ }
+}
+
+function setUp(){
+ cwdSetUp
+ junestSetUp
+
+ # Attempt to source the files under test to revert variable
+ # overrides (i.e. SH variable)
+ source "$JUNEST_ROOT/lib/core/common.sh"
+ source "$JUNEST_ROOT/lib/core/namespace.sh"
+ set +e
+
+ init_mocks
+}
+
+function tearDown(){
+ junestTearDown
+ cwdTearDown
+}
+
+function _test_copy_common_files() {
+ [[ -e /etc/hosts ]] && assertEquals "$(cat /etc/hosts)" "$(cat "${JUNEST_HOME}"/etc/hosts)"
+ [[ -e /etc/host.conf ]] && assertEquals "$(cat /etc/host.conf)" "$(cat "${JUNEST_HOME}"/etc/host.conf)"
+ [[ -e /etc/nsswitch.conf ]] && assertEquals "$(cat /etc/nsswitch.conf)" "$(cat "${JUNEST_HOME}"/etc/nsswitch.conf)"
+ [[ -e /etc/resolv.conf ]] && assertEquals "$(cat /etc/resolv.conf)" "$(cat "${JUNEST_HOME}"/etc/resolv.conf)"
+}
+
+function _test_copy_remaining_files() {
+ [[ -e /etc/hosts.equiv ]] && assertEquals "$(cat /etc/hosts.equiv)" "$(cat "${JUNEST_HOME}"/etc/hosts.equiv)"
+ [[ -e /etc/netgroup ]] && assertEquals "$(cat /etc/netgroup)" "$(cat "${JUNEST_HOME}"/etc/netgroup)"
+ [[ -e /etc/networks ]] && assertEquals "$(cat /etc/networks)" "$(cat "${JUNEST_HOME}"/etc/networks)"
+
+ [[ -e ${JUNEST_HOME}/etc/passwd ]]
+ assertEquals 0 $?
+ [[ -e ${JUNEST_HOME}/etc/group ]]
+ assertEquals 0 $?
+}
+
+function test_is_user_namespace_enabled_no_config_file(){
+ PROC_USERNS_FILE="blah"
+ PROC_USERNS_CLONE_FILE="blah"
+ CONFIG_PROC_FILE="blah"
+ CONFIG_BOOT_FILE="blah"
+ assertCommandFailOnStatus "$NOT_EXISTING_FILE" _is_user_namespace_enabled
+}
+
+function test_is_user_namespace_enabled_no_config(){
+ PROC_USERNS_FILE="blah"
+ PROC_USERNS_CLONE_FILE="blah"
+ touch config
+ gzip config
+ # shellcheck disable=SC2034
+ CONFIG_PROC_FILE="config.gz"
+ # shellcheck disable=SC2034
+ CONFIG_BOOT_FILE="blah"
+ assertCommandFailOnStatus "$NO_CONFIG_FOUND" _is_user_namespace_enabled
+}
+
+function test_is_user_namespace_enabled_with_userns_clone_file_disabled(){
+ PROC_USERNS_FILE="blah"
+ PROC_USERNS_CLONE_FILE="unprivileged_userns_clone"
+ echo "0" > $PROC_USERNS_CLONE_FILE
+ assertCommandFailOnStatus "$UNPRIVILEGED_USERNS_DISABLED" _is_user_namespace_enabled
+}
+
+function test_is_user_namespace_enabled_with_userns_clone_file_enabled(){
+ PROC_USERNS_CLONE_FILE="unprivileged_userns_clone"
+ echo "1" > $PROC_USERNS_CLONE_FILE
+ assertCommandSuccess _is_user_namespace_enabled
+}
+
+function test_is_user_namespace_enabled_with_proc_userns_file_existing(){
+ PROC_USERNS_FILE="user"
+ ln -s . $PROC_USERNS_FILE
+ PROC_USERNS_CLONE_FILE="blah"
+ assertCommandSuccess _is_user_namespace_enabled
+}
+
+function test_run_env_as_bwrap_fakeroot() {
+ assertCommandSuccess run_env_as_bwrap_fakeroot "" "" "false"
+ assertEquals "$BWRAP $COMMON_BWRAP_OPTION --cap-add ALL --uid 0 --gid 0 sudo /bin/sh --login" "$(cat "$STDOUTF")"
+
+ _test_copy_common_files
+}
+
+function test_run_env_as_bwrap_fakeroot_with_backend_command() {
+ assertCommandSuccess run_env_as_bwrap_fakeroot "mybwrap" "" "false"
+ assertEquals "mybwrap $COMMON_BWRAP_OPTION --cap-add ALL --uid 0 --gid 0 sudo /bin/sh --login" "$(cat "$STDOUTF")"
+
+ _test_copy_common_files
+}
+
+function test_run_env_as_bwrap_user() {
+ assertCommandSuccess run_env_as_bwrap_user "" "" "false"
+ assertEquals "$BWRAP $COMMON_BWRAP_OPTION /bin/sh --login" "$(cat "$STDOUTF")"
+
+ _test_copy_common_files
+ _test_copy_remaining_files
+}
+
+function test_run_env_as_bwrap_user_with_backend_command() {
+ assertCommandSuccess run_env_as_bwrap_user "mybwrap" "" "false"
+ assertEquals "mybwrap $COMMON_BWRAP_OPTION /bin/sh --login" "$(cat "$STDOUTF")"
+
+ _test_copy_common_files
+ _test_copy_remaining_files
+}
+
+function test_run_env_as_bwrap_fakeroot_no_copy() {
+ assertCommandSuccess run_env_as_bwrap_fakeroot "" "" "true" ""
+ assertEquals "$BWRAP $COMMON_BWRAP_OPTION --cap-add ALL --uid 0 --gid 0 sudo /bin/sh --login" "$(cat "$STDOUTF")"
+
+ [[ ! -e ${JUNEST_HOME}/etc/hosts ]]
+ assertEquals 0 $?
+ [[ ! -e ${JUNEST_HOME}/etc/host.conf ]]
+ assertEquals 0 $?
+ [[ ! -e ${JUNEST_HOME}/etc/nsswitch.conf ]]
+ assertEquals 0 $?
+ [[ ! -e ${JUNEST_HOME}/etc/resolv.conf ]]
+ assertEquals 0 $?
+
+ [[ ! -e ${JUNEST_HOME}/etc/hosts.equiv ]]
+ assertEquals 0 $?
+ [[ ! -e ${JUNEST_HOME}/etc/netgroup ]]
+ assertEquals 0 $?
+ [[ ! -e ${JUNEST_HOME}/etc/networks ]]
+ assertEquals 0 $?
+
+ [[ ! -e ${JUNEST_HOME}/etc/passwd ]]
+ assertEquals 0 $?
+ [[ ! -e ${JUNEST_HOME}/etc/group ]]
+ assertEquals 0 $?
+}
+
+function test_run_env_as_bwrap_user_no_copy() {
+ assertCommandSuccess run_env_as_bwrap_user "" "" "true" ""
+ assertEquals "$BWRAP $COMMON_BWRAP_OPTION /bin/sh --login" "$(cat "$STDOUTF")"
+
+ [[ ! -e ${JUNEST_HOME}/etc/hosts ]]
+ assertEquals 0 $?
+ [[ ! -e ${JUNEST_HOME}/etc/host.conf ]]
+ assertEquals 0 $?
+ [[ ! -e ${JUNEST_HOME}/etc/nsswitch.conf ]]
+ assertEquals 0 $?
+ [[ ! -e ${JUNEST_HOME}/etc/resolv.conf ]]
+ assertEquals 0 $?
+
+ [[ ! -e ${JUNEST_HOME}/etc/hosts.equiv ]]
+ assertEquals 0 $?
+ [[ ! -e ${JUNEST_HOME}/etc/netgroup ]]
+ assertEquals 0 $?
+ [[ ! -e ${JUNEST_HOME}/etc/networks ]]
+ assertEquals 0 $?
+
+ [[ ! -e ${JUNEST_HOME}/etc/passwd ]]
+ assertEquals 0 $?
+ [[ ! -e ${JUNEST_HOME}/etc/group ]]
+ assertEquals 0 $?
+}
+
+function test_run_env_as_bwrap_fakeroot_with_backend_args() {
+ assertCommandSuccess run_env_as_bwrap_fakeroot "" "--bind /usr /usr" "false"
+ assertEquals "$BWRAP $COMMON_BWRAP_OPTION --cap-add ALL --uid 0 --gid 0 --bind /usr /usr sudo /bin/sh --login" "$(cat "$STDOUTF")"
+
+ _test_copy_common_files
+}
+
+function test_run_env_as_bwrap_user_with_backend_args() {
+ assertCommandSuccess run_env_as_bwrap_user "" "--bind /usr /usr" "false"
+ assertEquals "$BWRAP $COMMON_BWRAP_OPTION --bind /usr /usr /bin/sh --login" "$(cat "$STDOUTF")"
+
+ _test_copy_common_files
+ _test_copy_remaining_files
+}
+
+function test_run_env_as_bwrap_fakeroot_with_command() {
+ assertCommandSuccess run_env_as_bwrap_fakeroot "" "" "false" "ls -la"
+ assertEquals "$BWRAP $COMMON_BWRAP_OPTION --cap-add ALL --uid 0 --gid 0 sudo /bin/sh --login -c \"ls -la\"" "$(cat "$STDOUTF")"
+
+ _test_copy_common_files
+}
+
+function test_run_env_as_bwrap_user_with_command() {
+ assertCommandSuccess run_env_as_bwrap_user "" "" "false" "ls -la"
+ assertEquals "$BWRAP $COMMON_BWRAP_OPTION /bin/sh --login -c \"ls -la\"" "$(cat "$STDOUTF")"
+
+ _test_copy_common_files
+ _test_copy_remaining_files
+}
+
+function test_run_env_as_bwrap_fakeroot_with_backend_args_and_command() {
+ assertCommandSuccess run_env_as_bwrap_fakeroot "" "--bind /usr /usr" "false" "ls -la"
+ assertEquals "$BWRAP $COMMON_BWRAP_OPTION --cap-add ALL --uid 0 --gid 0 --bind /usr /usr sudo /bin/sh --login -c \"ls -la\"" "$(cat "$STDOUTF")"
+
+ _test_copy_common_files
+}
+
+function test_run_env_as_bwrap_user_with_backend_args_and_command() {
+ assertCommandSuccess run_env_as_bwrap_user "" "--bind /usr /usr" "false" "ls -la"
+ assertEquals "$BWRAP $COMMON_BWRAP_OPTION --bind /usr /usr /bin/sh --login -c \"ls -la\"" "$(cat "$STDOUTF")"
+
+ _test_copy_common_files
+ _test_copy_remaining_files
+}
+
+function test_run_env_as_bwrap_fakeroot_nested_env(){
+ JUNEST_ENV=1
+ assertCommandFailOnStatus 106 run_env_as_bwrap_fakeroot "" "" "false" ""
+ unset JUNEST_ENV
+}
+
+function test_run_env_as_bwrap_user_nested_env(){
+ # shellcheck disable=SC2034
+ JUNEST_ENV=1
+ assertCommandFailOnStatus 106 run_env_as_bwrap_user "" "" "false" ""
+ unset JUNEST_ENV
+}
+
+source "$JUNEST_ROOT"/tests/utils/shunit2
diff --git a/tests/unit-tests/test-proot.sh b/tests/unit-tests/test-proot.sh
new file mode 100755
index 0000000..0f4f11a
--- /dev/null
+++ b/tests/unit-tests/test-proot.sh
@@ -0,0 +1,220 @@
+#!/bin/bash
+# shellcheck disable=SC1091
+
+JUNEST_ROOT=$(readlink -f "$(dirname "$0")"/../..)
+
+source "$JUNEST_ROOT/tests/utils/utils.sh"
+
+source "$JUNEST_ROOT/lib/utils/utils.sh"
+
+# Disable the exiterr
+set +e
+
+function oneTimeSetUp(){
+ setUpUnitTests
+}
+
+function setUp(){
+ cwdSetUp
+ junestSetUp
+
+ # Attempt to source the files under test to revert variable
+ # overrides (i.e. SH variable)
+ source "$JUNEST_ROOT/lib/core/common.sh"
+ source "$JUNEST_ROOT/lib/core/proot.sh"
+ set +e
+}
+
+function tearDown(){
+ junestTearDown
+ cwdTearDown
+}
+
+function _test_copy_common_files() {
+ [[ -e /etc/hosts ]] && assertEquals "$(cat /etc/hosts)" "$(cat "${JUNEST_HOME}"/etc/hosts)"
+ [[ -e /etc/host.conf ]] && assertEquals "$(cat /etc/host.conf)" "$(cat "${JUNEST_HOME}"/etc/host.conf)"
+ [[ -e /etc/nsswitch.conf ]] && assertEquals "$(cat /etc/nsswitch.conf)" "$(cat "${JUNEST_HOME}"/etc/nsswitch.conf)"
+ [[ -e /etc/resolv.conf ]] && assertEquals "$(cat /etc/resolv.conf)" "$(cat "${JUNEST_HOME}"/etc/resolv.conf)"
+}
+
+function _test_copy_remaining_files() {
+ [[ -e /etc/hosts.equiv ]] && assertEquals "$(cat /etc/hosts.equiv)" "$(cat "${JUNEST_HOME}"/etc/hosts.equiv)"
+ [[ -e /etc/netgroup ]] && assertEquals "$(cat /etc/netgroup)" "$(cat "${JUNEST_HOME}"/etc/netgroup)"
+ [[ -e /etc/networks ]] && assertEquals "$(cat /etc/networks)" "$(cat "${JUNEST_HOME}"/etc/networks)"
+
+ [[ -e ${JUNEST_HOME}/etc/passwd ]]
+ assertEquals 0 $?
+ [[ -e ${JUNEST_HOME}/etc/group ]]
+ assertEquals 0 $?
+}
+
+function test_run_env_as_proot_user(){
+ # shellcheck disable=SC2317
+ _run_env_with_qemu() {
+ # shellcheck disable=SC2086
+ # shellcheck disable=SC2048
+ echo $*
+ }
+ assertCommandSuccess run_env_as_proot_user "" "-k 3.10" "false" "/usr/bin/mkdir" "-v" "/newdir2"
+ assertEquals "-b /run/user/$(id -u) -b $HOME -b /tmp -b /proc -b /sys -b /dev -r ${JUNEST_HOME} -k 3.10 /usr/bin/mkdir -v /newdir2" "$(cat "$STDOUTF")"
+
+ SH=("/usr/bin/echo")
+ assertCommandSuccess run_env_as_proot_user "" "-k 3.10" "false"
+ assertEquals "-b /run/user/$(id -u) -b $HOME -b /tmp -b /proc -b /sys -b /dev -r ${JUNEST_HOME} -k 3.10" "$(cat "$STDOUTF")"
+
+ _test_copy_common_files
+ _test_copy_remaining_files
+}
+
+function test_run_env_as_proot_user_with_backend_command(){
+ # shellcheck disable=SC2317
+ _run_env_with_qemu() {
+ # shellcheck disable=SC2086
+ # shellcheck disable=SC2048
+ echo $*
+ }
+ assertCommandSuccess run_env_as_proot_user "myproot" "-k 3.10" "false" "/usr/bin/mkdir" "-v" "/newdir2"
+ assertEquals "myproot -b /run/user/$(id -u) -b $HOME -b /tmp -b /proc -b /sys -b /dev -r ${JUNEST_HOME} -k 3.10 /usr/bin/mkdir -v /newdir2" "$(cat "$STDOUTF")"
+
+ SH=("/usr/bin/echo")
+ assertCommandSuccess run_env_as_proot_user "myproot" "-k 3.10" "false"
+ assertEquals "myproot -b /run/user/$(id -u) -b $HOME -b /tmp -b /proc -b /sys -b /dev -r ${JUNEST_HOME} -k 3.10" "$(cat "$STDOUTF")"
+
+ _test_copy_common_files
+ _test_copy_remaining_files
+}
+
+function test_run_env_as_proot_user_no_copy(){
+ # shellcheck disable=SC2317
+ _run_env_with_qemu() {
+ # shellcheck disable=SC2086
+ # shellcheck disable=SC2048
+ echo $*
+ }
+ assertCommandSuccess run_env_as_proot_user "" "-k 3.10" "true" "/usr/bin/mkdir" "-v" "/newdir2"
+ assertEquals "-b /run/user/$(id -u) -b $HOME -b /tmp -b /proc -b /sys -b /dev -r ${JUNEST_HOME} -k 3.10 /usr/bin/mkdir -v /newdir2" "$(cat "$STDOUTF")"
+
+ [[ ! -e ${JUNEST_HOME}/etc/hosts ]]
+ assertEquals 0 $?
+ [[ ! -e ${JUNEST_HOME}/etc/host.conf ]]
+ assertEquals 0 $?
+ [[ ! -e ${JUNEST_HOME}/etc/nsswitch.conf ]]
+ assertEquals 0 $?
+ [[ ! -e ${JUNEST_HOME}/etc/resolv.conf ]]
+ assertEquals 0 $?
+
+ [[ ! -e ${JUNEST_HOME}/etc/hosts.equiv ]]
+ assertEquals 0 $?
+ [[ ! -e ${JUNEST_HOME}/etc/netgroup ]]
+ assertEquals 0 $?
+ [[ ! -e ${JUNEST_HOME}/etc/networks ]]
+ assertEquals 0 $?
+
+ [[ ! -e ${JUNEST_HOME}/etc/passwd ]]
+ assertEquals 0 $?
+ [[ ! -e ${JUNEST_HOME}/etc/group ]]
+ assertEquals 0 $?
+}
+
+function test_run_env_as_proot_user_nested_env(){
+ JUNEST_ENV=1
+ assertCommandFailOnStatus 106 run_env_as_proot_user "" "" "false"
+ unset JUNEST_ENV
+}
+
+function test_run_env_as_proot_fakeroot(){
+ # shellcheck disable=SC2317
+ _run_env_with_qemu() {
+ # shellcheck disable=SC2086
+ # shellcheck disable=SC2048
+ echo $*
+ }
+ assertCommandSuccess run_env_as_proot_fakeroot "" "-k 3.10" "false" "/usr/bin/mkdir" "-v" "/newdir2"
+ assertEquals "-0 -b /run/user/$(id -u) -b ${HOME} -b /tmp -b /proc -b /sys -b /dev -r ${JUNEST_HOME} -k 3.10 /usr/bin/mkdir -v /newdir2" "$(cat "$STDOUTF")"
+
+ SH=("/usr/bin/echo")
+ assertCommandSuccess run_env_as_proot_fakeroot "" "-k 3.10" "false"
+ assertEquals "-0 -b /run/user/$(id -u) -b ${HOME} -b /tmp -b /proc -b /sys -b /dev -r ${JUNEST_HOME} -k 3.10" "$(cat "$STDOUTF")"
+
+ _test_copy_common_files
+}
+
+function test_run_env_as_proot_fakeroot_with_backend_command(){
+ # shellcheck disable=SC2317
+ _run_env_with_qemu() {
+ # shellcheck disable=SC2086
+ # shellcheck disable=SC2048
+ echo $*
+ }
+ assertCommandSuccess run_env_as_proot_fakeroot "myproot" "-k 3.10" "false" "/usr/bin/mkdir" "-v" "/newdir2"
+ assertEquals "myproot -0 -b /run/user/$(id -u) -b ${HOME} -b /tmp -b /proc -b /sys -b /dev -r ${JUNEST_HOME} -k 3.10 /usr/bin/mkdir -v /newdir2" "$(cat "$STDOUTF")"
+
+ # shellcheck disable=SC2034
+ SH=("/usr/bin/echo")
+ assertCommandSuccess run_env_as_proot_fakeroot "myproot" "-k 3.10" "false"
+ assertEquals "myproot -0 -b /run/user/$(id -u) -b ${HOME} -b /tmp -b /proc -b /sys -b /dev -r ${JUNEST_HOME} -k 3.10" "$(cat "$STDOUTF")"
+
+ _test_copy_common_files
+}
+
+function test_run_env_as_proot_fakeroot_nested_env(){
+ JUNEST_ENV=1
+ assertCommandFailOnStatus 106 run_env_as_proot_fakeroot "" "" "false" ""
+ unset JUNEST_ENV
+}
+
+function test_run_env_with_quotes(){
+ # shellcheck disable=SC2317
+ _run_env_with_qemu() {
+ # shellcheck disable=SC2086
+ # shellcheck disable=SC2048
+ echo $*
+ }
+ assertCommandSuccess run_env_as_proot_user "" "-k 3.10" "false" "bash" "-c" "/usr/bin/mkdir -v /newdir2"
+ assertEquals "-b /run/user/$(id -u) -b ${HOME} -b /tmp -b /proc -b /sys -b /dev -r ${JUNEST_HOME} -k 3.10 bash -c /usr/bin/mkdir -v /newdir2" "$(cat "$STDOUTF")"
+}
+
+function test_run_env_with_proot_args(){
+ # shellcheck disable=SC2317
+ proot_cmd() {
+ [ "$JUNEST_ENV" != "1" ] && return 1
+ # shellcheck disable=SC2086
+ # shellcheck disable=SC2048
+ echo $*
+ }
+
+ assertCommandSuccess _run_env_with_proot "" "--help"
+ assertEquals "--help /bin/sh --login" "$(cat "$STDOUTF")"
+
+ assertCommandSuccess _run_env_with_proot "" "--help" mycommand
+ assertEquals "--help /bin/sh --login -c mycommand" "$(cat "$STDOUTF")"
+
+ assertCommandFail _run_env_with_proot
+}
+
+function test_qemu() {
+ echo "JUNEST_ARCH=arm" > "${JUNEST_HOME}"/etc/junest/info
+ # shellcheck disable=SC2317
+ rm_cmd() {
+ # shellcheck disable=SC2086
+ # shellcheck disable=SC2048
+ echo $*
+ }
+ # shellcheck disable=SC2317
+ ln_cmd() {
+ # shellcheck disable=SC2086
+ # shellcheck disable=SC2048
+ echo $*
+ }
+ # shellcheck disable=SC2317
+ _run_env_with_proot() {
+ # shellcheck disable=SC2086
+ # shellcheck disable=SC2048
+ echo $*
+ }
+
+ RANDOM=100 ARCH=x86_64 assertCommandSuccess _run_env_with_qemu "" ""
+ assertEquals "$(echo -e "-s $JUNEST_HOME/bin/qemu-arm-static-x86_64 /tmp/qemu-arm-static-x86_64-100\n-q /tmp/qemu-arm-static-x86_64-100")" "$(cat "$STDOUTF")"
+}
+
+source "$JUNEST_ROOT"/tests/utils/shunit2
diff --git a/tests/unit-tests/test-setup.sh b/tests/unit-tests/test-setup.sh
new file mode 100755
index 0000000..de2df75
--- /dev/null
+++ b/tests/unit-tests/test-setup.sh
@@ -0,0 +1,85 @@
+#!/bin/bash
+# shellcheck disable=SC1091
+
+JUNEST_ROOT=$(readlink -f "$(dirname "$0")"/../..)
+
+source "$JUNEST_ROOT/tests/utils/utils.sh"
+
+source "$JUNEST_ROOT/lib/utils/utils.sh"
+source "$JUNEST_ROOT/lib/core/common.sh"
+source "$JUNEST_ROOT/lib/core/setup.sh"
+
+# Disable the exiterr
+set +e
+
+function oneTimeSetUp(){
+ setUpUnitTests
+}
+
+function setUp(){
+ cwdSetUp
+ junestSetUp
+}
+
+function tearDown(){
+ junestTearDown
+ cwdTearDown
+}
+
+function test_is_env_installed(){
+ rm -rf "${JUNEST_HOME:?}"/*
+ assertCommandFail is_env_installed
+ touch "$JUNEST_HOME"/just_file
+ assertCommandSuccess is_env_installed
+}
+
+function test_setup_env(){
+ rm -rf "${JUNEST_HOME:?}"/*
+ # shellcheck disable=SC2317
+ wget_mock(){
+ # Proof that the setup is happening
+ # inside $JUNEST_TEMPDIR
+ local cwd=${PWD#"${JUNEST_TEMPDIR}"}
+ local parent_dir=${PWD%"${cwd}"}
+ assertEquals "$JUNEST_TEMPDIR" "${parent_dir}"
+ touch file
+ tar -czvf "${CMD}-${ARCH}".tar.gz file
+ }
+ # shellcheck disable=SC2034
+ WGET=wget_mock
+ # shellcheck disable=SC2119
+ setup_env 1> /dev/null
+ assertTrue "[ -e $JUNEST_HOME/file ]"
+
+ assertCommandFailOnStatus 102 setup_env "noarch"
+}
+
+
+function test_setup_env_from_file(){
+ rm -rf "${JUNEST_HOME:?}"/*
+ touch file
+ tar -czvf "${CMD}-${ARCH}".tar.gz file 1> /dev/null
+ assertCommandSuccess setup_env_from_file "${CMD}-${ARCH}.tar.gz"
+ assertTrue "[ -e $JUNEST_HOME/file ]"
+}
+
+function test_setup_env_from_file_not_existing_file(){
+ assertCommandFailOnStatus 103 setup_env_from_file noexist.tar.gz
+}
+
+function test_setup_env_from_file_with_absolute_path(){
+ rm -rf "${JUNEST_HOME:?}"/*
+ touch file
+ tar -czf "${CMD}-${ARCH}".tar.gz file
+ assertCommandSuccess setup_env_from_file "${CMD}-${ARCH}.tar.gz"
+ assertTrue "[ -e $JUNEST_HOME/file ]"
+}
+
+function test_delete_env(){
+ echo "N" | delete_env 1> /dev/null
+ assertCommandSuccess is_env_installed
+ echo "Y" | delete_env 1> /dev/null
+ assertCommandFail is_env_installed
+}
+
+source "$JUNEST_ROOT"/tests/utils/shunit2
diff --git a/tests/unit-tests/test-utils.sh b/tests/unit-tests/test-utils.sh
new file mode 100755
index 0000000..03e602a
--- /dev/null
+++ b/tests/unit-tests/test-utils.sh
@@ -0,0 +1,105 @@
+#!/bin/bash
+# shellcheck disable=SC1091
+
+source "$(dirname "$0")/../utils/utils.sh"
+
+unset HOME
+export HOME
+HOME=$(TMPDIR=/tmp mktemp -d -t pearl-user-home.XXXXXXX)
+
+source "$(dirname "$0")/../../lib/utils/utils.sh"
+
+# Disable the exiterr
+set +e
+
+function oneTimeSetUp(){
+ setUpUnitTests
+}
+
+function test_check_not_null(){
+ assertCommandFailOnStatus 11 check_not_null "" ""
+ assertCommandSuccess check_not_null "bla" ""
+}
+
+function test_echoerr(){
+ assertCommandSuccess echoerr "Test"
+ assertEquals "Test" "$(cat "$STDERRF")"
+}
+
+function test_error(){
+ assertCommandSuccess error "Test"
+ local expected
+ expected=$(echo -e "\033[1;31mTest\033[0m")
+ assertEquals "$expected" "$(cat "$STDERRF")"
+}
+
+function test_warn(){
+ assertCommandSuccess warn "Test"
+ local expected
+ expected=$(echo -e "\033[1;33mTest\033[0m")
+ assertEquals "$expected" "$(cat "$STDERRF")"
+}
+
+function test_info(){
+ assertCommandSuccess info "Test"
+ local expected
+ expected=$(echo -e "\033[1;36mTest\033[0m")
+ assertEquals "$expected" "$(cat "$STDOUTF")"
+}
+
+function test_die(){
+ assertCommandFail die "Test"
+ local expected
+ expected=$(echo -e "\033[1;31mTest\033[0m")
+ assertEquals "$expected" "$(cat "$STDERRF")"
+}
+
+function test_die_on_status(){
+ assertCommandFailOnStatus 222 die_on_status 222 "Test"
+ local expected
+ expected=$(echo -e "\033[1;31mTest\033[0m")
+ assertEquals "$expected" "$(cat "$STDERRF")"
+}
+
+function test_ask_null_question(){
+ assertCommandFailOnStatus 11 ask "" "Y"
+}
+
+function test_ask(){
+ echo "Y" | ask "Test" &> /dev/null
+ assertEquals 0 $?
+ echo "y" | ask "Test" &> /dev/null
+ assertEquals 0 $?
+ echo "N" | ask "Test" &> /dev/null
+ assertEquals 1 $?
+ echo "n" | ask "Test" &> /dev/null
+ assertEquals 1 $?
+ echo -e "\n" | ask "Test" &> /dev/null
+ assertEquals 0 $?
+ echo -e "\n" | ask "Test" "N" &> /dev/null
+ assertEquals 1 $?
+ echo -e "asdf\n\n" | ask "Test" "N" &> /dev/null
+ assertEquals 1 $?
+}
+
+function test_ask_wrong_default_answer() {
+ echo "Y" | ask "Test" G &> /dev/null
+ assertEquals 33 $?
+}
+
+function test_insert_quotes_on_spaces(){
+ assertCommandSuccess insert_quotes_on_spaces this is "a test"
+ assertEquals "this is \"a test\"" "$(cat "$STDOUTF")"
+
+ assertCommandSuccess insert_quotes_on_spaces this is 'a test'
+ assertEquals "this is \"a test\"" "$(cat "$STDOUTF")"
+}
+
+function test_contains_element(){
+ array=("something to search for" "a string" "test2000")
+ assertCommandSuccess contains_element "a string" "${array[@]}"
+
+ assertCommandFailOnStatus 1 contains_element "blabla" "${array[@]}"
+}
+
+source "$(dirname "$0")"/../utils/shunit2
diff --git a/tests/unit-tests/test-wrappers.sh b/tests/unit-tests/test-wrappers.sh
new file mode 100755
index 0000000..ee9776f
--- /dev/null
+++ b/tests/unit-tests/test-wrappers.sh
@@ -0,0 +1,139 @@
+#!/bin/bash
+# shellcheck disable=SC1091
+
+source "$(dirname "$0")/../utils/utils.sh"
+
+source "$(dirname "$0")/../../lib/core/wrappers.sh"
+
+# Disable the exiterr
+set +e
+
+function oneTimeSetUp(){
+ setUpUnitTests
+}
+
+function setUp(){
+ junestSetUp
+}
+
+function tearDown(){
+ junestTearDown
+}
+
+function test_create_wrappers_empty_bin(){
+ assertCommandSuccess create_wrappers
+ assertEquals "" "$(cat "$STDOUTF")"
+ assertTrue "bin_wrappers does not exist" "[ -e $JUNEST_HOME/usr/bin_wrappers ]"
+}
+
+function test_create_wrappers_not_executable_file(){
+ touch "$JUNEST_HOME"/usr/bin/myfile
+ assertCommandSuccess create_wrappers
+ assertEquals "" "$(cat "$STDOUTF")"
+ assertTrue "bin_wrappers should exist" "[ -e $JUNEST_HOME/usr/bin_wrappers ]"
+ assertTrue "myfile wrapper should not exist" "[ ! -x $JUNEST_HOME/usr/bin_wrappers/myfile ]"
+}
+
+function test_create_wrappers_directory(){
+ mkdir -p "$JUNEST_HOME"/usr/bin/mydir
+ assertCommandSuccess create_wrappers
+ assertEquals "" "$(cat "$STDOUTF")"
+ assertTrue "bin_wrappers should exist" "[ -e $JUNEST_HOME/usr/bin_wrappers ]"
+ assertTrue "mydir wrapper should not exist" "[ ! -e $JUNEST_HOME/usr/bin_wrappers/mydir ]"
+}
+
+function test_create_wrappers_broken_link(){
+ ln -s /opt/myapp/bin/cmd "$JUNEST_HOME"/usr/bin/cmd
+ assertCommandSuccess create_wrappers
+ assertEquals "" "$(cat "$STDOUTF")"
+ assertTrue "bin_wrappers should exist" "[ -e $JUNEST_HOME/usr/bin_wrappers ]"
+ assertTrue "cmd wrapper should exist" "[ -x $JUNEST_HOME/usr/bin_wrappers/cmd ]"
+}
+
+function test_create_wrappers_executable_file(){
+ touch "$JUNEST_HOME"/usr/bin/myfile
+ chmod +x "$JUNEST_HOME"/usr/bin/myfile
+ assertCommandSuccess create_wrappers
+ assertEquals "" "$(cat "$STDOUTF")"
+ assertTrue "bin_wrappers should exist" "[ -e $JUNEST_HOME/usr/bin_wrappers ]"
+ assertTrue "myfile wrapper should exist" "[ -x $JUNEST_HOME/usr/bin_wrappers/myfile ]"
+}
+
+function test_create_wrappers_verify_content(){
+ # Test for:
+ # https://github.com/fsquillace/junest/issues/262
+ # https://github.com/fsquillace/junest/issues/292
+ touch "$JUNEST_HOME"/usr/bin/myfile
+ chmod +x "$JUNEST_HOME"/usr/bin/myfile
+ export JUNEST_ARGS="ns --fakeroot -b '--bind /run /run2'"
+ assertCommandSuccess create_wrappers
+ assertEquals "" "$(cat "$STDOUTF")"
+
+ # Mock junest command to capture the actual output generated from myfile script
+ # shellcheck disable=SC2317
+ junest(){
+ for arg in "$@"
+ do
+ echo "$arg"
+ done
+ }
+ assertEquals "ns
+--fakeroot
+-b
+--bind /run /run2
+--
+test-wrappers.sh
+pacman
+-Rsn
+neovim
+new package" "$(source "$JUNEST_HOME"/usr/bin_wrappers/myfile pacman -Rsn neovim 'new package')"
+}
+
+function test_create_wrappers_already_exist(){
+ touch "$JUNEST_HOME"/usr/bin/myfile
+ chmod +x "$JUNEST_HOME"/usr/bin/myfile
+ mkdir -p "$JUNEST_HOME"/usr/bin_wrappers
+ echo "original" > "$JUNEST_HOME"/usr/bin_wrappers/myfile
+ chmod +x "$JUNEST_HOME"/usr/bin_wrappers/myfile
+ assertCommandSuccess create_wrappers false
+ assertEquals "" "$(cat "$STDOUTF")"
+ assertTrue "bin_wrappers should exist" "[ -e $JUNEST_HOME/usr/bin_wrappers ]"
+ assertTrue "myfile wrapper should exist" "[ -x $JUNEST_HOME/usr/bin_wrappers/myfile ]"
+ assertEquals "original" "$(cat "$JUNEST_HOME"/usr/bin_wrappers/myfile)"
+}
+
+function test_create_wrappers_forced_already_exist(){
+ echo "new" > "$JUNEST_HOME"/usr/bin/myfile
+ chmod +x "$JUNEST_HOME"/usr/bin/myfile
+ mkdir -p "$JUNEST_HOME"/usr/bin_wrappers
+ echo "original" > "$JUNEST_HOME"/usr/bin_wrappers/myfile
+ chmod +x "$JUNEST_HOME"/usr/bin_wrappers/myfile
+ assertCommandSuccess create_wrappers true
+ assertEquals "" "$(cat "$STDOUTF")"
+ assertTrue "bin_wrappers should exist" "[ -e $JUNEST_HOME/usr/bin_wrappers ]"
+ assertTrue "myfile wrapper should exist" "[ -x $JUNEST_HOME/usr/bin_wrappers/myfile ]"
+ assertNotEquals "original" "$(cat "$JUNEST_HOME"/usr/bin_wrappers/myfile)"
+}
+
+function test_create_wrappers_executable_no_longer_exist(){
+ mkdir -p "$JUNEST_HOME"/usr/bin_wrappers
+ touch "$JUNEST_HOME"/usr/bin_wrappers/myfile
+ chmod +x "$JUNEST_HOME"/usr/bin_wrappers/myfile
+ assertCommandSuccess create_wrappers
+ assertEquals "" "$(cat "$STDOUTF")"
+ assertTrue "bin_wrappers should exist" "[ -e $JUNEST_HOME/usr/bin_wrappers ]"
+ assertTrue "myfile wrapper should not exist" "[ ! -x $JUNEST_HOME/usr/bin_wrappers/myfile ]"
+}
+
+function test_create_wrappers_custom_bin_path(){
+ mkdir -p "$JUNEST_HOME"/usr/mybindir
+ touch "$JUNEST_HOME"/usr/mybindir/myfile
+ chmod +x "$JUNEST_HOME"/usr/mybindir/myfile
+ assertCommandSuccess create_wrappers false /usr/mybindir/
+ assertEquals "" "$(cat "$STDOUTF")"
+ assertTrue "bin_wrappers should exist" "[ -e $JUNEST_HOME/usr/mybindir_wrappers ]"
+ assertTrue "myfile wrapper should exist" "[ -x $JUNEST_HOME/usr/mybindir_wrappers/myfile ]"
+}
+
+
+source "$(dirname "$0")"/../utils/shunit2
diff --git a/tests/unit-tests/unit-tests.sh b/tests/unit-tests/unit-tests.sh
new file mode 100755
index 0000000..e90ed22
--- /dev/null
+++ b/tests/unit-tests/unit-tests.sh
@@ -0,0 +1,9 @@
+#!/bin/bash
+tests_succeded=true
+# shellcheck disable=SC2010
+for tst in $(ls "$(dirname "$0")"/test* | grep -v "$(basename "$0")")
+do
+ $tst || tests_succeded=false
+done
+
+$tests_succeded
diff --git a/tests/utils/shunit2 b/tests/utils/shunit2
new file mode 100644
index 0000000..e4c719c
--- /dev/null
+++ b/tests/utils/shunit2
@@ -0,0 +1,1067 @@
+#! /bin/sh
+# $Id$
+# vim:et:ft=sh:sts=2:sw=2
+#
+# Copyright 2008 Kate Ward. All Rights Reserved.
+# Released under the LGPL (GNU Lesser General Public License)
+#
+# shUnit2 -- Unit testing framework for Unix shell scripts.
+# http://code.google.com/p/shunit2/
+#
+# Author: kate.ward@forestent.com (Kate Ward)
+#
+# shUnit2 is a xUnit based unit test framework for Bourne shell scripts. It is
+# based on the popular JUnit unit testing framework for Java.
+
+# return if shunit already loaded
+[ -n "${SHUNIT_VERSION:-}" ] && exit 0
+SHUNIT_VERSION='2.1.7pre'
+
+# return values that scripts can use
+SHUNIT_TRUE=0
+SHUNIT_FALSE=1
+SHUNIT_ERROR=2
+
+# logging functions
+_shunit_warn() { echo "shunit2:WARN $@" >&2; }
+_shunit_error() { echo "shunit2:ERROR $@" >&2; }
+_shunit_fatal() { echo "shunit2:FATAL $@" >&2; exit ${SHUNIT_ERROR}; }
+
+# determine some reasonable command defaults
+__SHUNIT_UNAME_S=`uname -s`
+case "${__SHUNIT_UNAME_S}" in
+ BSD) __SHUNIT_EXPR_CMD='gexpr' ;;
+ *) __SHUNIT_EXPR_CMD='expr' ;;
+esac
+
+# commands a user can override if needed
+SHUNIT_EXPR_CMD=${SHUNIT_EXPR_CMD:-${__SHUNIT_EXPR_CMD}}
+
+# enable strict mode by default
+SHUNIT_STRICT=${SHUNIT_STRICT:-${SHUNIT_TRUE}}
+
+# specific shell checks
+if [ -n "${ZSH_VERSION:-}" ]; then
+ setopt |grep "^shwordsplit$" >/dev/null
+ if [ $? -ne ${SHUNIT_TRUE} ]; then
+ _shunit_fatal 'zsh shwordsplit option is required for proper operation'
+ fi
+ if [ -z "${SHUNIT_PARENT:-}" ]; then
+ _shunit_fatal "zsh does not pass \$0 through properly. please declare \
+\"SHUNIT_PARENT=\$0\" before calling shUnit2"
+ fi
+fi
+
+#
+# constants
+#
+
+__SHUNIT_ASSERT_MSG_PREFIX='ASSERT:'
+__SHUNIT_MODE_SOURCED='sourced'
+__SHUNIT_MODE_STANDALONE='standalone'
+__SHUNIT_PARENT=${SHUNIT_PARENT:-$0}
+
+# set the constants readonly
+__shunit_constants=`set |grep '^__SHUNIT_' |cut -d= -f1`
+echo "${__shunit_constants}" |grep '^Binary file' >/dev/null && \
+ __shunit_constants=`set |grep -a '^__SHUNIT_' |cut -d= -f1`
+for __shunit_const in ${__shunit_constants}; do
+ if [ -z "${ZSH_VERSION:-}" ]; then
+ readonly ${__shunit_const}
+ else
+ case ${ZSH_VERSION} in
+ [123].*) readonly ${__shunit_const} ;;
+ *) readonly -g ${__shunit_const} # declare readonly constants globally
+ esac
+ fi
+done
+unset __shunit_const __shunit_constants
+
+#
+# internal variables
+#
+
+# variables
+__shunit_lineno='' # line number of executed test
+__shunit_mode=${__SHUNIT_MODE_SOURCED} # operating mode
+__shunit_reportGenerated=${SHUNIT_FALSE} # is report generated
+__shunit_script='' # filename of unittest script (standalone mode)
+__shunit_skip=${SHUNIT_FALSE} # is skipping enabled
+__shunit_suite='' # suite of tests to execute
+
+# counts of tests
+__shunit_testSuccess=${SHUNIT_TRUE}
+__shunit_testsTotal=0
+__shunit_testsPassed=0
+__shunit_testsFailed=0
+
+# counts of asserts
+__shunit_assertsTotal=0
+__shunit_assertsPassed=0
+__shunit_assertsFailed=0
+__shunit_assertsSkipped=0
+
+# macros
+_SHUNIT_LINENO_='eval __shunit_lineno=""; if [ "${1:-}" = "--lineno" ]; then [ -n "$2" ] && __shunit_lineno="[$2] "; shift 2; fi'
+
+#-----------------------------------------------------------------------------
+# private functions
+
+#-----------------------------------------------------------------------------
+# assert functions
+#
+
+# Assert that two values are equal to one another.
+#
+# Args:
+# message: string: failure message [optional]
+# expected: string: expected value
+# actual: string: actual value
+# Returns:
+# integer: success (TRUE/FALSE/ERROR constant)
+assertEquals()
+{
+ ${_SHUNIT_LINENO_}
+ if [ $# -lt 2 -o $# -gt 3 ]; then
+ _shunit_error "assertEquals() requires two or three arguments; $# given"
+ _shunit_error "1: ${1:+$1} 2: ${2:+$2} 3: ${3:+$3}${4:+ 4: $4}"
+ return ${SHUNIT_ERROR}
+ fi
+ _shunit_shouldSkip && return ${SHUNIT_TRUE}
+
+ shunit_message_=${__shunit_lineno}
+ if [ $# -eq 3 ]; then
+ shunit_message_="${shunit_message_}$1"
+ shift
+ fi
+ shunit_expected_=$1
+ shunit_actual_=$2
+
+ shunit_return=${SHUNIT_TRUE}
+ if [ "${shunit_expected_}" = "${shunit_actual_}" ]; then
+ _shunit_assertPass
+ else
+ failNotEquals "${shunit_message_}" "${shunit_expected_}" "${shunit_actual_}"
+ shunit_return=${SHUNIT_FALSE}
+ fi
+
+ unset shunit_message_ shunit_expected_ shunit_actual_
+ return ${shunit_return}
+}
+_ASSERT_EQUALS_='eval assertEquals --lineno "${LINENO:-}"'
+
+# Assert that two values are not equal to one another.
+#
+# Args:
+# message: string: failure message [optional]
+# expected: string: expected value
+# actual: string: actual value
+# Returns:
+# integer: success (TRUE/FALSE/ERROR constant)
+assertNotEquals()
+{
+ ${_SHUNIT_LINENO_}
+ if [ $# -lt 2 -o $# -gt 3 ]; then
+ _shunit_error "assertNotEquals() requires two or three arguments; $# given"
+ return ${SHUNIT_ERROR}
+ fi
+ _shunit_shouldSkip && return ${SHUNIT_TRUE}
+
+ shunit_message_=${__shunit_lineno}
+ if [ $# -eq 3 ]; then
+ shunit_message_="${shunit_message_}$1"
+ shift
+ fi
+ shunit_expected_=$1
+ shunit_actual_=$2
+
+ shunit_return=${SHUNIT_TRUE}
+ if [ "${shunit_expected_}" != "${shunit_actual_}" ]; then
+ _shunit_assertPass
+ else
+ failSame "${shunit_message_}" "$@"
+ shunit_return=${SHUNIT_FALSE}
+ fi
+
+ unset shunit_message_ shunit_expected_ shunit_actual_
+ return ${shunit_return}
+}
+_ASSERT_NOT_EQUALS_='eval assertNotEquals --lineno "${LINENO:-}"'
+
+# Assert that a value is null (i.e. an empty string)
+#
+# Args:
+# message: string: failure message [optional]
+# actual: string: actual value
+# Returns:
+# integer: success (TRUE/FALSE/ERROR constant)
+assertNull()
+{
+ ${_SHUNIT_LINENO_}
+ if [ $# -lt 1 -o $# -gt 2 ]; then
+ _shunit_error "assertNull() requires one or two arguments; $# given"
+ return ${SHUNIT_ERROR}
+ fi
+ _shunit_shouldSkip && return ${SHUNIT_TRUE}
+
+ shunit_message_=${__shunit_lineno}
+ if [ $# -eq 2 ]; then
+ shunit_message_="${shunit_message_}$1"
+ shift
+ fi
+ assertTrue "${shunit_message_}" "[ -z '$1' ]"
+ shunit_return=$?
+
+ unset shunit_message_
+ return ${shunit_return}
+}
+_ASSERT_NULL_='eval assertNull --lineno "${LINENO:-}"'
+
+# Assert that a value is not null (i.e. a non-empty string)
+#
+# Args:
+# message: string: failure message [optional]
+# actual: string: actual value
+# Returns:
+# integer: success (TRUE/FALSE/ERROR constant)
+assertNotNull()
+{
+ ${_SHUNIT_LINENO_}
+ if [ $# -gt 2 ]; then # allowing 0 arguments as $1 might actually be null
+ _shunit_error "assertNotNull() requires one or two arguments; $# given"
+ return ${SHUNIT_ERROR}
+ fi
+ _shunit_shouldSkip && return ${SHUNIT_TRUE}
+
+ shunit_message_=${__shunit_lineno}
+ if [ $# -eq 2 ]; then
+ shunit_message_="${shunit_message_}$1"
+ shift
+ fi
+ shunit_actual_=`_shunit_escapeCharactersInString "${1:-}"`
+ test -n "${shunit_actual_}"
+ assertTrue "${shunit_message_}" $?
+ shunit_return=$?
+
+ unset shunit_actual_ shunit_message_
+ return ${shunit_return}
+}
+_ASSERT_NOT_NULL_='eval assertNotNull --lineno "${LINENO:-}"'
+
+# Assert that two values are the same (i.e. equal to one another).
+#
+# Args:
+# message: string: failure message [optional]
+# expected: string: expected value
+# actual: string: actual value
+# Returns:
+# integer: success (TRUE/FALSE/ERROR constant)
+assertSame()
+{
+ ${_SHUNIT_LINENO_}
+ if [ $# -lt 2 -o $# -gt 3 ]; then
+ _shunit_error "assertSame() requires two or three arguments; $# given"
+ return ${SHUNIT_ERROR}
+ fi
+ _shunit_shouldSkip && return ${SHUNIT_TRUE}
+
+ shunit_message_=${__shunit_lineno}
+ if [ $# -eq 3 ]; then
+ shunit_message_="${shunit_message_}$1"
+ shift
+ fi
+ assertEquals "${shunit_message_}" "$1" "$2"
+ shunit_return=$?
+
+ unset shunit_message_
+ return ${shunit_return}
+}
+_ASSERT_SAME_='eval assertSame --lineno "${LINENO:-}"'
+
+# Assert that two values are not the same (i.e. not equal to one another).
+#
+# Args:
+# message: string: failure message [optional]
+# expected: string: expected value
+# actual: string: actual value
+# Returns:
+# integer: success (TRUE/FALSE/ERROR constant)
+assertNotSame()
+{
+ ${_SHUNIT_LINENO_}
+ if [ $# -lt 2 -o $# -gt 3 ]; then
+ _shunit_error "assertNotSame() requires two or three arguments; $# given"
+ return ${SHUNIT_ERROR}
+ fi
+ _shunit_shouldSkip && return ${SHUNIT_TRUE}
+
+ shunit_message_=${__shunit_lineno}
+ if [ $# -eq 3 ]; then
+ shunit_message_="${shunit_message_:-}$1"
+ shift
+ fi
+ assertNotEquals "${shunit_message_}" "$1" "$2"
+ shunit_return=$?
+
+ unset shunit_message_
+ return ${shunit_return}
+}
+_ASSERT_NOT_SAME_='eval assertNotSame --lineno "${LINENO:-}"'
+
+# Assert that a value or shell test condition is true.
+#
+# In shell, a value of 0 is true and a non-zero value is false. Any integer
+# value passed can thereby be tested.
+#
+# Shell supports much more complicated tests though, and a means to support
+# them was needed. As such, this function tests that conditions are true or
+# false through evaluation rather than just looking for a true or false.
+#
+# The following test will succeed:
+# assertTrue 0
+# assertTrue "[ 34 -gt 23 ]"
+# The folloing test will fail with a message:
+# assertTrue 123
+# assertTrue "test failed" "[ -r '/non/existant/file' ]"
+#
+# Args:
+# message: string: failure message [optional]
+# condition: string: integer value or shell conditional statement
+# Returns:
+# integer: success (TRUE/FALSE/ERROR constant)
+assertTrue()
+{
+ ${_SHUNIT_LINENO_}
+ if [ $# -lt 1 -o $# -gt 2 ]; then
+ _shunit_error "assertTrue() takes one or two arguments; $# given"
+ return ${SHUNIT_ERROR}
+ fi
+ _shunit_shouldSkip && return ${SHUNIT_TRUE}
+
+ shunit_message_=${__shunit_lineno}
+ if [ $# -eq 2 ]; then
+ shunit_message_="${shunit_message_}$1"
+ shift
+ fi
+ shunit_condition_=$1
+
+ # see if condition is an integer, i.e. a return value
+ shunit_match_=`expr "${shunit_condition_}" : '\([0-9]*\)'`
+ shunit_return=${SHUNIT_TRUE}
+ if [ -z "${shunit_condition_}" ]; then
+ # null condition
+ shunit_return=${SHUNIT_FALSE}
+ elif [ -n "${shunit_match_}" -a "${shunit_condition_}" = "${shunit_match_}" ]
+ then
+ # possible return value. treating 0 as true, and non-zero as false.
+ [ ${shunit_condition_} -ne 0 ] && shunit_return=${SHUNIT_FALSE}
+ else
+ # (hopefully) a condition
+ ( eval ${shunit_condition_} ) >/dev/null 2>&1
+ [ $? -ne 0 ] && shunit_return=${SHUNIT_FALSE}
+ fi
+
+ # record the test
+ if [ ${shunit_return} -eq ${SHUNIT_TRUE} ]; then
+ _shunit_assertPass
+ else
+ _shunit_assertFail "${shunit_message_}"
+ fi
+
+ unset shunit_message_ shunit_condition_ shunit_match_
+ return ${shunit_return}
+}
+_ASSERT_TRUE_='eval assertTrue --lineno "${LINENO:-}"'
+
+# Assert that a value or shell test condition is false.
+#
+# In shell, a value of 0 is true and a non-zero value is false. Any integer
+# value passed can thereby be tested.
+#
+# Shell supports much more complicated tests though, and a means to support
+# them was needed. As such, this function tests that conditions are true or
+# false through evaluation rather than just looking for a true or false.
+#
+# The following test will succeed:
+# assertFalse 1
+# assertFalse "[ 'apples' = 'oranges' ]"
+# The folloing test will fail with a message:
+# assertFalse 0
+# assertFalse "test failed" "[ 1 -eq 1 -a 2 -eq 2 ]"
+#
+# Args:
+# message: string: failure message [optional]
+# condition: string: integer value or shell conditional statement
+# Returns:
+# integer: success (TRUE/FALSE/ERROR constant)
+assertFalse()
+{
+ ${_SHUNIT_LINENO_}
+ if [ $# -lt 1 -o $# -gt 2 ]; then
+ _shunit_error "assertFalse() quires one or two arguments; $# given"
+ return ${SHUNIT_ERROR}
+ fi
+ _shunit_shouldSkip && return ${SHUNIT_TRUE}
+
+ shunit_message_=${__shunit_lineno}
+ if [ $# -eq 2 ]; then
+ shunit_message_="${shunit_message_}$1"
+ shift
+ fi
+ shunit_condition_=$1
+
+ # see if condition is an integer, i.e. a return value
+ shunit_match_=`expr "${shunit_condition_}" : '\([0-9]*\)'`
+ shunit_return=${SHUNIT_TRUE}
+ if [ -z "${shunit_condition_}" ]; then
+ # null condition
+ shunit_return=${SHUNIT_FALSE}
+ elif [ -n "${shunit_match_}" -a "${shunit_condition_}" = "${shunit_match_}" ]
+ then
+ # possible return value. treating 0 as true, and non-zero as false.
+ [ ${shunit_condition_} -eq 0 ] && shunit_return=${SHUNIT_FALSE}
+ else
+ # (hopefully) a condition
+ ( eval ${shunit_condition_} ) >/dev/null 2>&1
+ [ $? -eq 0 ] && shunit_return=${SHUNIT_FALSE}
+ fi
+
+ # record the test
+ if [ ${shunit_return} -eq ${SHUNIT_TRUE} ]; then
+ _shunit_assertPass
+ else
+ _shunit_assertFail "${shunit_message_}"
+ fi
+
+ unset shunit_message_ shunit_condition_ shunit_match_
+ return ${shunit_return}
+}
+_ASSERT_FALSE_='eval assertFalse --lineno "${LINENO:-}"'
+
+#-----------------------------------------------------------------------------
+# failure functions
+#
+
+# Records a test failure.
+#
+# Args:
+# message: string: failure message [optional]
+# Returns:
+# integer: success (TRUE/FALSE/ERROR constant)
+fail()
+{
+ ${_SHUNIT_LINENO_}
+ if [ $# -gt 1 ]; then
+ _shunit_error "fail() requires zero or one arguments; $# given"
+ return ${SHUNIT_ERROR}
+ fi
+ _shunit_shouldSkip && return ${SHUNIT_TRUE}
+
+ shunit_message_=${__shunit_lineno}
+ if [ $# -eq 1 ]; then
+ shunit_message_="${shunit_message_}$1"
+ shift
+ fi
+
+ _shunit_assertFail "${shunit_message_}"
+
+ unset shunit_message_
+ return ${SHUNIT_FALSE}
+}
+_FAIL_='eval fail --lineno "${LINENO:-}"'
+
+# Records a test failure, stating two values were not equal.
+#
+# Args:
+# message: string: failure message [optional]
+# expected: string: expected value
+# actual: string: actual value
+# Returns:
+# integer: success (TRUE/FALSE/ERROR constant)
+failNotEquals()
+{
+ ${_SHUNIT_LINENO_}
+ if [ $# -lt 2 -o $# -gt 3 ]; then
+ _shunit_error "failNotEquals() requires one or two arguments; $# given"
+ return ${SHUNIT_ERROR}
+ fi
+ _shunit_shouldSkip && return ${SHUNIT_TRUE}
+
+ shunit_message_=${__shunit_lineno}
+ if [ $# -eq 3 ]; then
+ shunit_message_="${shunit_message_}$1"
+ shift
+ fi
+ shunit_expected_=$1
+ shunit_actual_=$2
+
+ _shunit_assertFail "${shunit_message_:+${shunit_message_} }expected:<${shunit_expected_}> but was:<${shunit_actual_}>"
+
+ unset shunit_message_ shunit_expected_ shunit_actual_
+ return ${SHUNIT_FALSE}
+}
+_FAIL_NOT_EQUALS_='eval failNotEquals --lineno "${LINENO:-}"'
+
+# Records a test failure, stating two values should have been the same.
+#
+# Args:
+# message: string: failure message [optional]
+# expected: string: expected value
+# actual: string: actual value
+# Returns:
+# integer: success (TRUE/FALSE/ERROR constant)
+failSame()
+{
+ ${_SHUNIT_LINENO_}
+ if [ $# -lt 2 -o $# -gt 3 ]; then
+ _shunit_error "failSame() requires two or three arguments; $# given"
+ return ${SHUNIT_ERROR}
+ fi
+ _shunit_shouldSkip && return ${SHUNIT_TRUE}
+
+ shunit_message_=${__shunit_lineno}
+ if [ $# -eq 3 ]; then
+ shunit_message_="${shunit_message_}$1"
+ shift
+ fi
+
+ _shunit_assertFail "${shunit_message_:+${shunit_message_} }expected not same"
+
+ unset shunit_message_
+ return ${SHUNIT_FALSE}
+}
+_FAIL_SAME_='eval failSame --lineno "${LINENO:-}"'
+
+# Records a test failure, stating two values were not equal.
+#
+# This is functionally equivalent to calling failNotEquals().
+#
+# Args:
+# message: string: failure message [optional]
+# expected: string: expected value
+# actual: string: actual value
+# Returns:
+# integer: success (TRUE/FALSE/ERROR constant)
+failNotSame()
+{
+ ${_SHUNIT_LINENO_}
+ if [ $# -lt 2 -o $# -gt 3 ]; then
+ _shunit_error "failNotEquals() requires one or two arguments; $# given"
+ return ${SHUNIT_ERROR}
+ fi
+ _shunit_shouldSkip && return ${SHUNIT_TRUE}
+
+ shunit_message_=${__shunit_lineno}
+ if [ $# -eq 3 ]; then
+ shunit_message_="${shunit_message_}$1"
+ shift
+ fi
+ failNotEquals "${shunit_message_}" "$1" "$2"
+ shunit_return=$?
+
+ unset shunit_message_
+ return ${shunit_return}
+}
+_FAIL_NOT_SAME_='eval failNotSame --lineno "${LINENO:-}"'
+
+#-----------------------------------------------------------------------------
+# skipping functions
+#
+
+# Force remaining assert and fail functions to be "skipped".
+#
+# This function forces the remaining assert and fail functions to be "skipped",
+# i.e. they will have no effect. Each function skipped will be recorded so that
+# the total of asserts and fails will not be altered.
+#
+# Args:
+# None
+startSkipping()
+{
+ __shunit_skip=${SHUNIT_TRUE}
+}
+
+# Resume the normal recording behavior of assert and fail calls.
+#
+# Args:
+# None
+endSkipping()
+{
+ __shunit_skip=${SHUNIT_FALSE}
+}
+
+# Returns the state of assert and fail call skipping.
+#
+# Args:
+# None
+# Returns:
+# boolean: (TRUE/FALSE constant)
+isSkipping()
+{
+ return ${__shunit_skip}
+}
+
+#-----------------------------------------------------------------------------
+# suite functions
+#
+
+# Stub. This function should contains all unit test calls to be made.
+#
+# DEPRECATED (as of 2.1.0)
+#
+# This function can be optionally overridden by the user in their test suite.
+#
+# If this function exists, it will be called when shunit2 is sourced. If it
+# does not exist, shunit2 will search the parent script for all functions
+# beginning with the word 'test', and they will be added dynamically to the
+# test suite.
+#
+# This function should be overridden by the user in their unit test suite.
+# Note: see _shunit_mktempFunc() for actual implementation
+#
+# Args:
+# None
+#suite() { :; } # DO NOT UNCOMMENT THIS FUNCTION
+
+# Adds a function name to the list of tests schedule for execution.
+#
+# This function should only be called from within the suite() function.
+#
+# Args:
+# function: string: name of a function to add to current unit test suite
+suite_addTest()
+{
+ shunit_func_=${1:-}
+
+ __shunit_suite="${__shunit_suite:+${__shunit_suite} }${shunit_func_}"
+ __shunit_testsTotal=`expr ${__shunit_testsTotal} + 1`
+
+ unset shunit_func_
+}
+
+# Stub. This function will be called once before any tests are run.
+#
+# Common one-time environment preparation tasks shared by all tests can be
+# defined here.
+#
+# This function should be overridden by the user in their unit test suite.
+# Note: see _shunit_mktempFunc() for actual implementation
+#
+# Args:
+# None
+#oneTimeSetUp() { :; } # DO NOT UNCOMMENT THIS FUNCTION
+
+# Stub. This function will be called once after all tests are finished.
+#
+# Common one-time environment cleanup tasks shared by all tests can be defined
+# here.
+#
+# This function should be overridden by the user in their unit test suite.
+# Note: see _shunit_mktempFunc() for actual implementation
+#
+# Args:
+# None
+#oneTimeTearDown() { :; } # DO NOT UNCOMMENT THIS FUNCTION
+
+# Stub. This function will be called before each test is run.
+#
+# Common environment preparation tasks shared by all tests can be defined here.
+#
+# This function should be overridden by the user in their unit test suite.
+# Note: see _shunit_mktempFunc() for actual implementation
+#
+# Args:
+# None
+#setUp() { :; }
+
+# Note: see _shunit_mktempFunc() for actual implementation
+# Stub. This function will be called after each test is run.
+#
+# Common environment cleanup tasks shared by all tests can be defined here.
+#
+# This function should be overridden by the user in their unit test suite.
+# Note: see _shunit_mktempFunc() for actual implementation
+#
+# Args:
+# None
+#tearDown() { :; } # DO NOT UNCOMMENT THIS FUNCTION
+
+#------------------------------------------------------------------------------
+# internal shUnit2 functions
+#
+
+# Create a temporary directory to store various run-time files in.
+#
+# This function is a cross-platform temporary directory creation tool. Not all
+# OSes have the mktemp function, so one is included here.
+#
+# Args:
+# None
+# Outputs:
+# string: the temporary directory that was created
+_shunit_mktempDir()
+{
+ # try the standard mktemp function
+ ( exec mktemp -dqt shunit.XXXXXX 2>/dev/null ) && return
+
+ # the standard mktemp didn't work. doing our own.
+ if [ -r '/dev/urandom' -a -x '/usr/bin/od' ]; then
+ _shunit_random_=`/usr/bin/od -vAn -N4 -tx4 "${_shunit_file_}"
+#! /bin/sh
+exit ${SHUNIT_TRUE}
+EOF
+ chmod +x "${_shunit_file_}"
+ done
+
+ unset _shunit_file_
+}
+
+# Final cleanup function to leave things as we found them.
+#
+# Besides removing the temporary directory, this function is in charge of the
+# final exit code of the unit test. The exit code is based on how the script
+# was ended (e.g. normal exit, or via Ctrl-C).
+#
+# Args:
+# name: string: name of the trap called (specified when trap defined)
+_shunit_cleanup()
+{
+ _shunit_name_=$1
+
+ case ${_shunit_name_} in
+ EXIT) _shunit_signal_=0 ;;
+ INT) _shunit_signal_=2 ;;
+ TERM) _shunit_signal_=15 ;;
+ *)
+ _shunit_warn "unrecognized trap value (${_shunit_name_})"
+ _shunit_signal_=0
+ ;;
+ esac
+
+ # do our work
+ rm -fr "${__shunit_tmpDir}"
+
+ # exit for all non-EXIT signals
+ if [ ${_shunit_name_} != 'EXIT' ]; then
+ _shunit_warn "trapped and now handling the (${_shunit_name_}) signal"
+ # disable EXIT trap
+ trap 0
+ # add 128 to signal and exit
+ exit `expr ${_shunit_signal_} + 128`
+ elif [ ${__shunit_reportGenerated} -eq ${SHUNIT_FALSE} ] ; then
+ _shunit_assertFail 'Unknown failure encountered running a test'
+ _shunit_generateReport
+ exit ${SHUNIT_ERROR}
+ fi
+
+ unset _shunit_name_ _shunit_signal_
+}
+
+# The actual running of the tests happens here.
+#
+# Args:
+# None
+_shunit_execSuite()
+{
+ for _shunit_test_ in ${__shunit_suite}; do
+ __shunit_testSuccess=${SHUNIT_TRUE}
+
+ # disable skipping
+ endSkipping
+
+ # execute the per-test setup function
+ setUp
+
+ # execute the test
+ echo "${_shunit_test_}"
+ eval ${_shunit_test_}
+
+ # execute the per-test tear-down function
+ tearDown
+
+ # update stats
+ if [ ${__shunit_testSuccess} -eq ${SHUNIT_TRUE} ]; then
+ __shunit_testsPassed=`expr ${__shunit_testsPassed} + 1`
+ else
+ __shunit_testsFailed=`expr ${__shunit_testsFailed} + 1`
+ fi
+ done
+
+ unset _shunit_test_
+}
+
+# Generates the user friendly report with appropriate OK/FAILED message.
+#
+# Args:
+# None
+# Output:
+# string: the report of successful and failed tests, as well as totals.
+_shunit_generateReport()
+{
+ _shunit_ok_=${SHUNIT_TRUE}
+
+ # if no exit code was provided one, determine an appropriate one
+ [ ${__shunit_testsFailed} -gt 0 \
+ -o ${__shunit_testSuccess} -eq ${SHUNIT_FALSE} ] \
+ && _shunit_ok_=${SHUNIT_FALSE}
+
+ echo
+ if [ ${__shunit_testsTotal} -eq 1 ]; then
+ echo "Ran ${__shunit_testsTotal} test."
+ else
+ echo "Ran ${__shunit_testsTotal} tests."
+ fi
+
+ _shunit_failures_=''
+ _shunit_skipped_=''
+ [ ${__shunit_assertsFailed} -gt 0 ] \
+ && _shunit_failures_="failures=${__shunit_assertsFailed}"
+ [ ${__shunit_assertsSkipped} -gt 0 ] \
+ && _shunit_skipped_="skipped=${__shunit_assertsSkipped}"
+
+ if [ ${_shunit_ok_} -eq ${SHUNIT_TRUE} ]; then
+ _shunit_msg_='OK'
+ [ -n "${_shunit_skipped_}" ] \
+ && _shunit_msg_="${_shunit_msg_} (${_shunit_skipped_})"
+ else
+ _shunit_msg_="FAILED (${_shunit_failures_}"
+ [ -n "${_shunit_skipped_}" ] \
+ && _shunit_msg_="${_shunit_msg_},${_shunit_skipped_}"
+ _shunit_msg_="${_shunit_msg_})"
+ fi
+
+ echo
+ echo ${_shunit_msg_}
+ __shunit_reportGenerated=${SHUNIT_TRUE}
+
+ unset _shunit_failures_ _shunit_msg_ _shunit_ok_ _shunit_skipped_
+}
+
+# Test for whether a function should be skipped.
+#
+# Args:
+# None
+# Returns:
+# boolean: whether the test should be skipped (TRUE/FALSE constant)
+_shunit_shouldSkip()
+{
+ [ ${__shunit_skip} -eq ${SHUNIT_FALSE} ] && return ${SHUNIT_FALSE}
+ _shunit_assertSkip
+}
+
+# Records a successful test.
+#
+# Args:
+# None
+_shunit_assertPass()
+{
+ __shunit_assertsPassed=`expr ${__shunit_assertsPassed} + 1`
+ __shunit_assertsTotal=`expr ${__shunit_assertsTotal} + 1`
+}
+
+# Records a test failure.
+#
+# Args:
+# message: string: failure message to provide user
+_shunit_assertFail()
+{
+ _shunit_msg_=$1
+
+ __shunit_testSuccess=${SHUNIT_FALSE}
+ __shunit_assertsFailed=`expr ${__shunit_assertsFailed} + 1`
+ __shunit_assertsTotal=`expr ${__shunit_assertsTotal} + 1`
+ echo "${__SHUNIT_ASSERT_MSG_PREFIX}${_shunit_msg_}"
+
+ unset _shunit_msg_
+}
+
+# Records a skipped test.
+#
+# Args:
+# None
+_shunit_assertSkip()
+{
+ __shunit_assertsSkipped=`expr ${__shunit_assertsSkipped} + 1`
+ __shunit_assertsTotal=`expr ${__shunit_assertsTotal} + 1`
+}
+
+# Prepare a script filename for sourcing.
+#
+# Args:
+# script: string: path to a script to source
+# Returns:
+# string: filename prefixed with ./ (if necessary)
+_shunit_prepForSourcing()
+{
+ _shunit_script_=$1
+ case "${_shunit_script_}" in
+ /*|./*) echo "${_shunit_script_}" ;;
+ *) echo "./${_shunit_script_}" ;;
+ esac
+ unset _shunit_script_
+}
+
+# Escape a character in a string.
+#
+# Args:
+# c: string: unescaped character
+# s: string: to escape character in
+# Returns:
+# string: with escaped character(s)
+_shunit_escapeCharInStr()
+{
+ [ -n "$2" ] || return # no point in doing work on an empty string
+
+ # Note: using shorter variable names to prevent conflicts with
+ # _shunit_escapeCharactersInString().
+ _shunit_c_=$1
+ _shunit_s_=$2
+
+
+ # escape the character
+ echo ''${_shunit_s_}'' |sed 's/\'${_shunit_c_}'/\\\'${_shunit_c_}'/g'
+
+ unset _shunit_c_ _shunit_s_
+}
+
+# Escape a character in a string.
+#
+# Args:
+# str: string: to escape characters in
+# Returns:
+# string: with escaped character(s)
+_shunit_escapeCharactersInString()
+{
+ [ -n "$1" ] || return # no point in doing work on an empty string
+
+ _shunit_str_=$1
+
+ # Note: using longer variable names to prevent conflicts with
+ # _shunit_escapeCharInStr().
+ for _shunit_char_ in '"' '$' "'" '`'; do
+ _shunit_str_=`_shunit_escapeCharInStr "${_shunit_char_}" "${_shunit_str_}"`
+ done
+
+ echo "${_shunit_str_}"
+ unset _shunit_char_ _shunit_str_
+}
+
+# Extract list of functions to run tests against.
+#
+# Args:
+# script: string: name of script to extract functions from
+# Returns:
+# string: of function names
+_shunit_extractTestFunctions()
+{
+ _shunit_script_=$1
+
+ # extract the lines with test function names, strip of anything besides the
+ # function name, and output everything on a single line.
+ _shunit_regex_='^[ ]*(function )*test[A-Za-z0-9_]* *\(\)'
+ grep -E "${_shunit_regex_}" "${_shunit_script_}" \
+ |sed 's/^[^A-Za-z0-9_]*//;s/^function //;s/\([A-Za-z0-9_]*\).*/\1/g' \
+ |xargs
+
+ unset _shunit_regex_ _shunit_script_
+}
+
+#------------------------------------------------------------------------------
+# main
+#
+
+# determine the operating mode
+if [ $# -eq 0 ]; then
+ __shunit_script=${__SHUNIT_PARENT}
+ __shunit_mode=${__SHUNIT_MODE_SOURCED}
+else
+ __shunit_script=$1
+ [ -r "${__shunit_script}" ] || \
+ _shunit_fatal "unable to read from ${__shunit_script}"
+ __shunit_mode=${__SHUNIT_MODE_STANDALONE}
+fi
+
+# create a temporary storage location
+__shunit_tmpDir=`_shunit_mktempDir`
+
+# provide a public temporary directory for unit test scripts
+# TODO(kward): document this
+SHUNIT_TMPDIR="${__shunit_tmpDir}/tmp"
+mkdir "${SHUNIT_TMPDIR}"
+
+# setup traps to clean up after ourselves
+trap '_shunit_cleanup EXIT' 0
+trap '_shunit_cleanup INT' 2
+trap '_shunit_cleanup TERM' 15
+
+# create phantom functions to work around issues with Cygwin
+_shunit_mktempFunc
+PATH="${__shunit_tmpDir}:${PATH}"
+
+# make sure phantom functions are executable. this will bite if /tmp (or the
+# current $TMPDIR) points to a path on a partition that was mounted with the
+# 'noexec' option. the noexec command was created with _shunit_mktempFunc().
+noexec 2>/dev/null || _shunit_fatal \
+ 'please declare TMPDIR with path on partition with exec permission'
+
+# we must manually source the tests in standalone mode
+if [ "${__shunit_mode}" = "${__SHUNIT_MODE_STANDALONE}" ]; then
+ . "`_shunit_prepForSourcing \"${__shunit_script}\"`"
+fi
+
+# execute the oneTimeSetUp function (if it exists)
+oneTimeSetUp
+
+# execute the suite function defined in the parent test script
+# deprecated as of 2.1.0
+suite
+
+# if no suite function was defined, dynamically build a list of functions
+if [ -z "${__shunit_suite}" ]; then
+ shunit_funcs_=`_shunit_extractTestFunctions "${__shunit_script}"`
+ for shunit_func_ in ${shunit_funcs_}; do
+ suite_addTest ${shunit_func_}
+ done
+fi
+unset shunit_func_ shunit_funcs_
+
+# execute the tests
+_shunit_execSuite
+
+# execute the oneTimeTearDown function (if it exists)
+oneTimeTearDown
+
+# generate the report
+_shunit_generateReport
+
+# that's it folks
+[ ${__shunit_testsFailed} -eq 0 ]
+exit $?
diff --git a/tests/utils/utils.sh b/tests/utils/utils.sh
new file mode 100644
index 0000000..ab37bd9
--- /dev/null
+++ b/tests/utils/utils.sh
@@ -0,0 +1,62 @@
+#!/usr/bin/env bash
+
+OLD_CWD=${PWD}
+function cwdSetUp(){
+ ORIGIN_CWD=$(TMPDIR=/tmp mktemp -d -t junest-cwd.XXXXXXXXXX)
+ cd "$ORIGIN_CWD" || return 1
+}
+
+function cwdTearDown(){
+ rm -rf "$ORIGIN_CWD"
+ cd "$OLD_CWD" || return 1
+}
+
+function junestSetUp(){
+ JUNEST_HOME=$(TMPDIR=/tmp mktemp -d -t junest-home.XXXXXXXXXX)
+ mkdir -p "${JUNEST_HOME}/usr/bin"
+ mkdir -p "${JUNEST_HOME}/etc/junest"
+ echo "JUNEST_ARCH=x86_64" > "${JUNEST_HOME}/etc/junest/info"
+ mkdir -p "${JUNEST_HOME}/etc/ca-certificates"
+}
+
+function junestTearDown(){
+ # the CA directories are read only and can be deleted only by changing the mod
+ [ -d "${JUNEST_HOME}/etc/ca-certificates" ] && chmod -R +w "${JUNEST_HOME}/etc/ca-certificates"
+ rm -rf "$JUNEST_HOME"
+ unset JUNEST_HOME
+}
+
+function setUpUnitTests(){
+ OUTPUT_DIR="${SHUNIT_TMPDIR}/output"
+ mkdir "${OUTPUT_DIR}"
+ STDOUTF="${OUTPUT_DIR}/stdout"
+ STDERRF="${OUTPUT_DIR}/stderr"
+}
+
+function assertCommandSuccess(){
+ # shellcheck disable=SC2091
+ $(set -e
+ "$@" > "$STDOUTF" 2> "$STDERRF"
+ )
+ assertTrue "The command $1 did not return 0 exit status" $?
+}
+
+function assertCommandFail(){
+ # shellcheck disable=SC2091
+ $(set -e
+ "$@" > "$STDOUTF" 2> "$STDERRF"
+ )
+ assertFalse "The command $1 returned 0 exit status" $?
+}
+
+# $1: expected exit status
+# $2-: The command under test
+function assertCommandFailOnStatus(){
+ local status=$1
+ shift
+ # shellcheck disable=SC2091
+ $(set -e
+ "$@" > "$STDOUTF" 2> "$STDERRF"
+ )
+ assertEquals "$status" $?
+}