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. + +

+ JuNest +

+ +|Project Status|Donation|Communication| +|:------------:|:------:|:-----------:| +| [![Build status](https://api.travis-ci.com/fsquillace/junest.png?branch=master)](https://app.travis-ci.com/github/fsquillace/junest) [![OpenHub](https://www.openhub.net/p/junest/widgets/project_thin_badge.gif)](https://www.openhub.net/p/junest) | [![Github Sponsors](https://img.shields.io/badge/GitHub-Sponsors-orange.svg)](https://github.com/sponsors/fsquillace) [![PayPal](https://img.shields.io/badge/PayPal-Donation-blue.svg)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=8LEHQKBCYTACY) [![Buy me a coffee](https://www.buymeacoffee.com/assets/img/custom_images/yellow_img.png)](https://www.buymeacoffee.com/fsquillace) | [![Join the Discord server at https://discord.gg/ttfBT7MKve](https://img.shields.io/badge/Discord-Server-blueviolet.svg)](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/images/0)](https://sourcerer.io/fame/fsquillace/fsquillace/junest/links/0)[![](https://sourcerer.io/fame/fsquillace/fsquillace/junest/images/1)](https://sourcerer.io/fame/fsquillace/fsquillace/junest/links/1)[![](https://sourcerer.io/fame/fsquillace/fsquillace/junest/images/2)](https://sourcerer.io/fame/fsquillace/fsquillace/junest/links/2)[![](https://sourcerer.io/fame/fsquillace/fsquillace/junest/images/3)](https://sourcerer.io/fame/fsquillace/fsquillace/junest/links/3)[![](https://sourcerer.io/fame/fsquillace/fsquillace/junest/images/4)](https://sourcerer.io/fame/fsquillace/fsquillace/junest/links/4)[![](https://sourcerer.io/fame/fsquillace/fsquillace/junest/images/5)](https://sourcerer.io/fame/fsquillace/fsquillace/junest/links/5)[![](https://sourcerer.io/fame/fsquillace/fsquillace/junest/images/6)](https://sourcerer.io/fame/fsquillace/fsquillace/junest/links/6)[![](https://sourcerer.io/fame/fsquillace/fsquillace/junest/images/7)](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" $? +}