diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..1d2dc0d --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,21 @@ +Copyright 2020 (c) 2020 Raspberry Pi (Trading) Ltd. + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following + disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products + derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF +THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..251d306 --- /dev/null +++ b/README.md @@ -0,0 +1,45 @@ +# Raspberry Pi Pico Setup + +Raspberry Pi Pico Setup provides a script for installing the Pico SDK and toolchain. + +## How-To + +Download and run `pico_setup.sh`: + +```shell +wget https://raw.githubusercontent.com/raspberrypi/pico-setup/master/pico_setup.sh +chmod +x pico_setup.sh +./pico_setup.sh +``` + +The script uses sudo, so you may need to enter your password. + +After the script is complete, reboot to ensure that all changes take effect, such as the UART settings and environment variables. + +If you want the testing script and documentation, you can clone the git repo too. + +## Support + +This script works on most Debian-derived Linux distros and macOS, running on common Raspberry Pi, PC, and Mac hardware. This ***DOESN'T*** mean that all of the pico tools work properly on these platforms. It just means that this script runs and passes its own tests. + +Operating systems: + +* Raspberry Pi OS (32-bit) +* Debian 10 (Buster) +* Ubuntu 20.04 or later +* macOS 11 (Big Sur) +* Windows Subsystem for Linux + +Hardware: + +* Any model of Raspberry Pi +* PC (x86_64) +* Mac (both Intel and Apple Silicon) + +Visual Studio Code may not run well on Raspberry Pi 1 and Zero because of their smaller RAM capacity, but the rest of the toolkit works fine. + +Other OSes and hardware _may_ work, but haven't been tested. Use at your own risk. + +## Testing + +See [test/README.md](test/README.md). diff --git a/pico_setup.sh b/pico_setup.sh index 4a928d7..853a08c 100755 --- a/pico_setup.sh +++ b/pico_setup.sh @@ -1,186 +1,549 @@ -#!/bin/bash +#!/usr/bin/env bash + +# Phase 0: Preflight check +# Verify baseline dependencies + +# Phase 1: Setup dev environment +# Install the software packages from APT or Homebrew +# Create a directory called pico +# Download the pico-sdk repository and submodules +# Define env variables: PICO_SDK_PATH +# On Raspberry Pi only: configure the UART for use with Raspberry Pi Pico + +# Phase 2: Setting up tutorial repos +# Download pico-examples, pico-extras, pico-playground repositories, and submodules +# Build the blink and hello_world examples + +# Phase 3: Recommended tools +# Download and build picotool (see Appendix B), and copy it to /usr/local/bin. +# Download and build picoprobe (see Appendix A) and OpenOCD +# Download and install Visual Studio Code and required extensions + # Exit on error set -e -if grep -q Raspberry /proc/cpuinfo; then - echo "Running on a Raspberry Pi" -else - echo "Not running on a Raspberry Pi. Use at your own risk!" -fi +# Trying to use a non-existent variable is an error +set -u + +# if printenv DEBUG >& /dev/null; then + # Show all commands + set -x + + env +# fi # Number of cores when running make JNUM=4 # Where will the output go? -OUTDIR="$(pwd)/pico" +if printenv TARGET_DIR; then + echo "Using target dir from \$TARGET_DIR: ${TARGET_DIR}" +else + TARGET_DIR="$(pwd)/pico" + echo "Using target dir: ${TARGET_DIR}" +fi + +linux() { + # Returns true iff this is running on Linux + uname | grep -q "^Linux$" + return ${?} +} + +raspbian() { + # Returns true iff this is running on Raspbian or close derivative such as Raspberry Pi OS, but not necessarily on a Raspberry Pi computer + grep -q '^NAME="Raspbian GNU/Linux"$' /etc/os-release + return ${?} +} + +debian() { + # Returns true iff this is running on Debian + grep -q '^NAME="Debian GNU/Linux"$' /etc/os-release + return ${?} +} + +ubuntu() { + # Returns true iff this is running on Ubuntu + grep -q '^NAME="Ubuntu"$' /etc/os-release + return ${?} +} + +mac() { + # Returns true iff this is running on macOS and presumably Apple hardware + uname | grep -q "^Darwin$" + return ${?} +} + +raspberry_pi() { + # Returns true iff this is running on a Raspberry Pi computer, regardless of the OS + if [ -f /proc/cpuinfo ]; then + grep -q "^Model\s*: Raspberry Pi" /proc/cpuinfo + return ${?} + fi + return 1 +} + +sudo_wrapper() { + # Some platforms have different needs for invoking sudo. This wrapper encapsulates that complexity. + # The output of this function should be a string on stdout, which will be used as a command. Example: + # `$(sudo_wrapper) whoami` + # The above may equate to: + # `sudo -i whoami` + + if [ "${USER}" = root ]; then + # if we're already root, don't sudo at all. Relevant to some Docker images that don't have sudo but already run as root. + return + fi + + # EC2 AMIs tend to have the user password unset, so you can't sudo without -i. It will cd /root, so you have to be + # careful with relative paths in the command. + echo sudo -i +} + +phase_0() { + # Preflight check + # Checks the baseline dependencies. If you don't have these, this script won't work. + echo "Entering phase 0: Preflight check" + + if mac; then + echo "Running on macOS" + if which brew >> /dev/null; then + echo "Found brew" + brew update + else + echo -e 'This script requires Homebrew, the missing package manager for macOS. See https://docs.brew.sh/Installation. For quick install, run:\n/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install.sh)"' + echo "Stopping." + exit 1 + fi + else + if linux; then + echo "Running on Linux" + else + echo "Platform $(uname) not recognized. Use at your own risk. Continuing as though this were Linux." + fi -# Install dependencies -GIT_DEPS="git" -SDK_DEPS="cmake gcc-arm-none-eabi gcc g++" -OPENOCD_DEPS="gdb-multiarch automake autoconf build-essential texinfo libtool libftdi-dev libusb-1.0-0-dev" -# Wget to download the deb -VSCODE_DEPS="wget" -UART_DEPS="minicom" + if which apt >> /dev/null; then + echo "Found apt" + $(sudo_wrapper) apt update + else + echo 'This script requires apt, the default package manager for Debian and Debian-derived distros such as Ubuntu and Raspberry Pi OS.' + echo "Stopping." + exit 1 + fi + fi +} -# Build full list of dependencies -DEPS="$GIT_DEPS $SDK_DEPS" +fail() { + # Outputs a failure message and exits with the error code output by the previous call. + # All args are echoed as a failure message. -if [[ "$SKIP_OPENOCD" == 1 ]]; then - echo "Skipping OpenOCD (debug support)" -else - DEPS="$DEPS $OPENOCD_DEPS" -fi + R="${?}" + echo "Validation failed! :'-(" + if [ ${*} ]; then + echo "${*}" + fi + exit ${R} +} -if [[ "$SKIP_VSCODE" == 1 ]]; then - echo "Skipping VSCODE" -else - DEPS="$DEPS $VSCODE_DEPS" -fi +validate_git_repo() { + # tests that the given relative path exists and is a git repo + git -C ${TARGET_DIR}/${1} status >& /dev/null || fail +} + +validate_toolchain_linux() { + # test that the expected packages are installed + dpkg-query -s git cmake gcc-arm-none-eabi build-essential gdb-multiarch automake autoconf build-essential texinfo libtool libftdi-dev libusb-1.0-0-dev >& /dev/null || fail +} -echo "Installing Dependencies" -sudo apt update -sudo apt install -y $DEPS +install_dev_env_deps_linux() { + # Install development environment dependencies for Linux -echo "Creating $OUTDIR" -# Create pico directory to put everything in -mkdir -p $OUTDIR -cd $OUTDIR + # Avoid a certain dependency by installing ssh-client without recommends. See + # https://github.com/raspberrypi/pico-setup/pull/20#discussion_r608793993 for details. + $(sudo_wrapper) apt install -y --no-install-recommends ssh-client -# Clone sw repos -GITHUB_PREFIX="https://github.com/raspberrypi/" -GITHUB_SUFFIX=".git" -SDK_BRANCH="master" + DEPS="autoconf automake build-essential cmake gcc-arm-none-eabi gdb-multiarch git libftdi-dev libtool libusb-1.0-0-dev minicom pkg-config python3 texinfo" + if debian || ubuntu; then + DEPS="${DEPS} libstdc++-arm-none-eabi-newlib" + fi + $(sudo_wrapper) apt install -y ${DEPS} +} + +brew_install_idempotent() { + # For some reason, brew install is not idempotent. This function succeeds even when the package is already installed. + brew list ${*} || brew install ${*} + return ${?} +} + +validate_toolchain_mac() { + # test that the expected packages are installed + brew list git cmake pkg-config libtool automake libusb wget pkg-config gcc texinfo arm-none-eabi-gcc >& /dev/null +} + +install_dev_env_deps_mac() { + # Install development environment dependencies for mac + + brew_install_idempotent ArmMbed/homebrew-formulae/arm-none-eabi-gcc automake cmake git libtool libusb gcc minicom pkg-config texinfo wget +} + +create_TARGET_DIR() { + # Creates ./pico directory if necessary + + mkdir -p "${TARGET_DIR}" +} + +clone_raspberrypi_repo() { + # Clones the given repo name from GitHub and inits any submodules + # $1 should be the full name of the repo, ex: pico-sdk + # $2 should be the branch name. Defaults to master. + # all other args are passed to git clone + REPO_NAME="${1}" + if shift && [ ${#} -gt 0 ]; then + BRANCH="${1}" + # Can't just say `shift` because `set -e` will think it's an error and terminate the script. + shift || true + else + BRANCH=master + fi -for REPO in sdk examples extras playground -do - DEST="$OUTDIR/pico-$REPO" + REPO_URL="https://github.com/raspberrypi/${REPO_NAME}.git" + REPO_DIR="${TARGET_DIR}/${REPO_NAME}" - if [ -d $DEST ]; then - echo "$DEST already exists so skipping" + if [ -d "${REPO_DIR}" ]; then + echo "${REPO_DIR} already exists. Updating." + git -C "${REPO_DIR}" pull --ff-only else - REPO_URL="${GITHUB_PREFIX}pico-${REPO}${GITHUB_SUFFIX}" - echo "Cloning $REPO_URL" - git clone -b $SDK_BRANCH $REPO_URL + echo "Cloning ${REPO_URL}" + if [ ${#} -gt 0 ]; then + git -C "${TARGET_DIR}" clone -b "${BRANCH}" "${REPO_URL}" ${*} + else + git -C "${TARGET_DIR}" clone -b "${BRANCH}" "${REPO_URL}" + fi # Any submodules - cd $DEST - git submodule update --init - cd $OUTDIR - - # Define PICO_SDK_PATH in ~/.bashrc - VARNAME="PICO_${REPO^^}_PATH" - echo "Adding $VARNAME to ~/.bashrc" - echo "export $VARNAME=$DEST" >> ~/.bashrc - export ${VARNAME}=$DEST - fi -done - -cd $OUTDIR - -# Pick up new variables we just defined -source ~/.bashrc - -# Build a couple of examples -cd "$OUTDIR/pico-examples" -mkdir build -cd build -cmake ../ -DCMAKE_BUILD_TYPE=Debug - -for e in blink hello_world -do - echo "Building $e" - cd $e - make -j$JNUM - cd .. -done - -cd $OUTDIR - -# Picoprobe and picotool -for REPO in picoprobe picotool -do - DEST="$OUTDIR/$REPO" - REPO_URL="${GITHUB_PREFIX}${REPO}${GITHUB_SUFFIX}" - git clone $REPO_URL - - # Build both - cd $DEST - mkdir build + git -C "${REPO_DIR}" submodule update --init + fi +} + +warn_for_bashrc() { + # Earlier versions of this script set environment variables in .bashrc. The location has moved to .profile or + # .zprofile. If the user has a .bashrc defining any pico dev env variables, they could conflict with the settings + # in the other files. This function raises a warning for the user. + + REGEX="^\s*export\s+\"?PICO_SDK_PATH=" + if grep -q "${REGEX}" ~/.bashrc; then + echo "Your ~/.bashrc file contains the following line, which may conflict with this script's settings. We recommend removing it to prevent possible issues." + echo -n " "; grep "${REGEX}" ~/.bashrc + fi +} + +set_env() { + # Permanently sets an environment variable by adding it to the current user's profile script + # The form of the arguments should be `FOO foo`, which sets the environment variable `FOO=foo` + NAME="${1}" + VALUE="${2}" + EXPR="${NAME}=${VALUE}" + + # detect appropriate file for setting env vars + if echo "${SHELL}" | grep -q zsh; then + # zsh detected + FILE=~/.zprofile + else + # sh, bash and others + FILE=~/.profile + fi + + # ensure that appends go to a new line + if [ -f "${FILE}" ]; then + if ! ( tail -n 1 "${FILE}" | grep -q "^$" ); then + # FILE exists but has no trailing newline. Adding newline. + echo >> "${FILE}" + fi + fi + + # set for now + export "${EXPR}" + + # set for later + REGEX="^\s*export\s+\"?${NAME}=" + if grep -q "${REGEX}" "${FILE}"; then + # Warn the user + echo "Your ${FILE} already contains the following environment variable definition(s):" + grep "${REGEX}" "${FILE}" + echo "This script would normally set the following line. We're adding it, but commented out, so that you can choose which you want." + echo "export \"${EXPR}\"" + # Write to file + echo "# pico_setup.sh commented out the following line because it conflicts with another line in this file. You should choose one or the other." >> "${FILE}" + echo "# export \"${EXPR}\"" >> "${FILE}" + else + echo "Setting env variable ${EXPR} in ${FILE}" + echo "export \"${EXPR}\"" >> "${FILE}" + fi +} + +validate_pico_sdk() { + validate_git_repo pico-sdk + + # test that the SDK env var is set and correct + test "${PICO_SDK_PATH}" = "${TARGET_DIR}/pico-sdk" || fail +} + +setup_sdk() { + # Download the SDK + clone_raspberrypi_repo pico-sdk + + # Set env var PICO_SDK_PATH + set_env PICO_SDK_PATH "${TARGET_DIR}/pico-sdk" +} + +validate_uart() { + # test that the UART is configured. Only works on Raspberry Pi OS on Raspberry Pi hardware. + dpkg-query -s minicom >& /dev/null || fail + grep -q "enable_uart=1" /boot/config.txt || fail + # note that the test for console=serial0 tests for the absence of a string + grep -q "console=serial0" /boot/cmdline.txt && fail +} + +enable_uart() { + # Enable UART + echo "Disabling Linux serial console (UART) so we can use it for pico" + $(sudo_wrapper) raspi-config nonint do_serial 2 + echo "You must run sudo reboot to finish UART setup" +} + +phase_1() { + # Setup minimum dev environment + echo "Entering phase 1: Setup minimum dev environment" + + if mac; then + install_dev_env_deps_mac + validate_toolchain_mac + else + install_dev_env_deps_linux + validate_toolchain_linux + fi + + create_TARGET_DIR + setup_sdk + validate_pico_sdk + + if raspberry_pi && which raspi-config >> /dev/null; then + enable_uart + validate_uart + else + echo "Not configuring UART, because either this is not a Raspberry Pi computer, or raspi-config is not available." + fi +} + +build_examples() { + # Build a couple of examples + echo "Building selected examples" + + # Save the working directory + pushd "${TARGET_DIR}/pico-examples" >> /dev/null + + mkdir -p build + cd build + cmake ../ -DCMAKE_BUILD_TYPE=Debug + + for EXAMPLE in blink hello_world; do + echo "Building $EXAMPLE" + cd "$EXAMPLE" + make -j${JNUM} + cd .. + done + + # Restore the working directory + popd >> /dev/null +} + +validate_pico_extras() { + validate_git_repo pico-extras +} + +validate_pico_examples() { + validate_git_repo pico-examples + + # test that blink is built + test -f ${TARGET_DIR}/pico-examples/build/blink/blink.uf2 || fail + + # test that hello_serial is built + test -f ${TARGET_DIR}/pico-examples/build/hello_world/serial/hello_serial.uf2 || fail + + # test that hello_usb is built + test -f ${TARGET_DIR}/pico-examples/build/hello_world/usb/hello_usb.uf2 || fail +} + +validate_pico_playground() { + validate_git_repo pico-playground +} + +phase_2() { + # Setup tutorial repos + echo "Entering phase 2: Setting up tutorial repos" + + for REPO_NAME in pico-examples pico-extras pico-playground; do + clone_raspberrypi_repo "${REPO_NAME}" + done + + build_examples + + validate_pico_examples + validate_pico_extras + validate_pico_playground +} + +validate_picotool() { + validate_git_repo picotool + + # test that the binary is built + test -x ${TARGET_DIR}/picotool/build/picotool || fail + + # test that picotool is installed in the expected location + test -x /usr/local/bin/picotool || fail +} + +setup_picotool() { + # Downloads, builds, and installs picotool + echo "Setting up picotool" + + # Save the working directory + pushd "${TARGET_DIR}" >> /dev/null + + clone_raspberrypi_repo picotool + cd "${TARGET_DIR}/picotool" + mkdir -p build cd build cmake ../ - make -j$JNUM + make -j${JNUM} - if [[ "$REPO" == "picotool" ]]; then - echo "Installing picotool to /usr/local/bin/picotool" - sudo cp picotool /usr/local/bin/ - fi + echo "Installing picotool to /usr/local/bin/picotool" + $(sudo_wrapper) cp "${TARGET_DIR}/picotool/build/picotool" /usr/local/bin/ - cd $OUTDIR -done + # Restore the working directory + popd >> /dev/null +} -if [ -d openocd ]; then - echo "openocd already exists so skipping" - SKIP_OPENOCD=1 -fi +validate_openocd() { + validate_git_repo openocd -if [[ "$SKIP_OPENOCD" == 1 ]]; then - echo "Won't build OpenOCD" -else - # Build OpenOCD - echo "Building OpenOCD" - cd $OUTDIR - # Should we include picoprobe support (which is a Pico acting as a debugger for another Pico) - INCLUDE_PICOPROBE=1 - OPENOCD_BRANCH="rp2040" - OPENOCD_CONFIGURE_ARGS="--enable-ftdi --enable-sysfsgpio --enable-bcm2835gpio" - if [[ "$INCLUDE_PICOPROBE" == 1 ]]; then - OPENOCD_BRANCH="picoprobe" - OPENOCD_CONFIGURE_ARGS="$OPENOCD_CONFIGURE_ARGS --enable-picoprobe" - fi - - git clone "${GITHUB_PREFIX}openocd${GITHUB_SUFFIX}" -b $OPENOCD_BRANCH --depth=1 - cd openocd + # test that the binary is built + test -x ${TARGET_DIR}/openocd/src/openocd || fail +} + +setup_openocd() { + # Download, build, and install OpenOCD for picoprobe and bit-banging without picoprobe + echo "Setting up OpenOCD" + + # Save the working directory + pushd "${TARGET_DIR}" >> /dev/null + + clone_raspberrypi_repo openocd picoprobe --depth=1 + cd "${TARGET_DIR}/openocd" ./bootstrap - ./configure $OPENOCD_CONFIGURE_ARGS - make -j$JNUM - sudo make install -fi + OPTS="--enable-ftdi --enable-bcm2835gpio --enable-picoprobe" + if linux; then + # sysfsgpio is only available on linux + OPTS="${OPTS} --enable-sysfsgpio" + fi + ./configure ${OPTS} + make -j${JNUM} + $(sudo_wrapper) make -C "${TARGET_DIR}/openocd" install -cd $OUTDIR + # Restore the working directory + popd >> /dev/null +} -# Liam needed to install these to get it working -EXTRA_VSCODE_DEPS="libx11-xcb1 libxcb-dri3-0 libdrm2 libgbm1 libegl-mesa0" -if [[ "$SKIP_VSCODE" == 1 ]]; then - echo "Won't include VSCODE" -else - if [ -f vscode.deb ]; then - echo "Skipping vscode as vscode.deb exists" +validate_picoprobe() { + validate_git_repo picoprobe || fail + + # test that the binary is built + test -f ${TARGET_DIR}/picoprobe/build/picoprobe.uf2 || fail +} + +setup_picoprobe() { + # Download and build picoprobe. Requires that OpenOCD is already setup + echo "Setting up picoprobe" + + # Save the working directory + pushd "${TARGET_DIR}" >> /dev/null + + clone_raspberrypi_repo picoprobe + cd "${TARGET_DIR}/picoprobe" + mkdir -p build + cd build + cmake .. + make -j${JNUM} + + # Restore the working directory + popd >> /dev/null +} + +validate_vscode_linux() { + dpkg-query -s code >& /dev/null || fail +} + +install_vscode_linux() { + # Install Visual Studio Code + + # VS Code is specially added to Raspberry Pi OS repos, but might not be present on Debian/Ubuntu. So we check first. + if ! apt-cache show code >& /dev/null; then + echo "It appears that your APT repos do not offer Visual Studio Code. Skipping." + return + fi + + echo "Installing Visual Studio Code" + + $(sudo_wrapper) apt install -y code + + # Get extensions + code --install-extension marus25.cortex-debug + code --install-extension ms-vscode.cmake-tools + code --install-extension ms-vscode.cpptools +} + +validate_vscode_mac() { + echo "Not yet implemented: testing Visual Studio Code on macOS" +} + +install_vscode_mac() { + echo "Not yet implemented: installing Visual Studio Code on macOS" +} + +phase_3() { + # Setup recommended tools + echo "Setting up recommended tools" + + setup_picotool + validate_picotool + + setup_openocd + validate_openocd + + setup_picoprobe + validate_picoprobe + + # Install Visual Studio Code + if mac; then + install_vscode_mac + validate_vscode_mac else - echo "Installing VSCODE" - if uname -m | grep -q aarch64; then - VSCODE_DEB="https://aka.ms/linux-arm64-deb" + if dpkg-query -s xserver-xorg >& /dev/null; then + install_vscode_linux + validate_vscode_linux else - VSCODE_DEB="https://aka.ms/linux-armhf-deb" + echo "Not installing Visual Studio Code because it looks like XWindows is not installed." fi + fi +} - wget -O vscode.deb $VSCODE_DEB - sudo apt install -y ./vscode.deb - sudo apt install -y $EXTRA_VSCODE_DEPS +main() { + phase_0 + phase_1 + phase_2 + phase_3 - # Get extensions - code --install-extension marus25.cortex-debug - code --install-extension ms-vscode.cmake-tools - code --install-extension ms-vscode.cpptools - fi -fi + echo "Congratulations, installation is complete. :D" +} -# Enable UART -if [[ "$SKIP_UART" == 1 ]]; then - echo "Skipping uart configuration" -else - sudo apt install -y $UART_DEPS - echo "Disabling Linux serial console (UART) so we can use it for pico" - sudo raspi-config nonint do_serial 2 - echo "You must run sudo reboot to finish UART setup" -fi +main diff --git a/test/README.md b/test/README.md new file mode 100644 index 0000000..1873290 --- /dev/null +++ b/test/README.md @@ -0,0 +1,69 @@ +# Testing pico-setup + +`pico_setup.sh` validates that it has the expected effect for your specific setup. However, the script needs to work on a variety of platforms, so we test a variety. + +***Be warned: the testing script is destructive.*** It configures the system in all kinds of ways to see if the script can handle it, and makes little attempt to put things back where they were before. These tests are expected to be run on a single-use system. You start with a fresh image at the beginning, and discard it at the end. + +The general testing process is: + +1. Start from a fresh OS image. +1.1. On Raspberry Pi OS, you'll need to start the SSH service if necessary: `sudo /sbin/service ssh start` +1. Push the contents of the pico-setup repo from your dev host: `scp -r ~/git/raspberrypi/pico-setup/ ${TEST_USER}@${TEST_HOST}:` +1. SSH to the test host and execute the test suite: `ssh ${TEST_USER}@${TEST_HOST} "export WRECK_THIS_COMPUTER=PLEASE_I_DESERVE_IT && pico-setup/test/test_local.sh" |& tee ${TEST_HOST}.log` + +The `tee` part captures all output and dumps it to a log file on the local host so you can investigate anything that went wrong. Additionally, logs for each test should be on the testing host, at `/tmp/test_*` + +If the test completes successfully, it will say that it's complete and return 0. If it fails, it will return non-zero. + +## Preparations + +For all tests on Raspberry Pi computers, start with a pristine image written by Raspberry Pi Imager. The focus is on more recent hardware such as Raspberry Pi 4, but this installer's dependencies are more about software than hardware, so it should work with any version of the hardware. + +Raspberry Pi is an ARM platform, so testing Raspberry Pi hardware technically covers ARM tests. However, the Linux distros built for Raspberry Pi hardware tend to be customized, so testing the mainline build is nonetheless relevant. + +The tests on non-Raspberry Pi computers can be executed on EC2 instances. The recommended EC2 instance sizes cost pennies/hour, except for mac1. mac1 costs 1.083 USD/hour in us-west-2, and requires dedicated host with minimum charge of 24 hours. You can launch and terminate instances all day on that dedicated host without cost, other than the hour it takes to clean up a terminated instance and launch a new one. + +Notes on EC2: + +* For the Linux x86 tests, t3.micro with standard 8 GiB EBS volume are sufficient. +* For the ARM tests, t4g.micro with standard 8 GiB EBS volume are sufficient. +* For the Windows Subsystem for Linux tests, t3.large with standard 30 GiB EBS volume are sufficient. +* For the official Ubuntu AMIs from Canonical, the default username is `ubuntu`. +* For the official Debian AMIs from Debian, the default username is `admin`. +* For the macOS and Windows AMIs from Amazon, the default username is `ec2-user`. + +## Summary + +* ✅ Means this version is tested and works on this platform. +* ? Means this version hasn't been tested on this platform. +* (blank) Means this platform isn't relevant. + +| | Raspberry Pi | x86 | ARM | +|-|-|-|-| +| Raspberry Pi OS | ✅ | ? | | +| Ubuntu 20 | ? | ✅ | ✅ | +| Debian 10 | ? | ✅ | ✅ | +| macOS 11 Big Sur | | ? | ? | +| Ubuntu on WSL | | ? | | +| Debian on WSL | | ? | | + +## Windows Subsystem for Linux + +Install WSL according to https://docs.microsoft.com/en-us/windows/wsl/install-on-server. + +Get your Linux distro here: https://docs.microsoft.com/en-us/windows/wsl/install-manual#downloading-distributions + +The Debian build apparently doesn't contain wget: + +```shell +sudo apt update +sudo apt install -y wget +``` + +In the Linux shell: + +```shell +wget https://raw.githubusercontent.com/raspberrypi/pico-setup/master/pico_setup.sh +chmod +x pico_setup.sh +./pico_setup.sh +``` diff --git a/test/test_local.sh b/test/test_local.sh new file mode 100755 index 0000000..f6bc320 --- /dev/null +++ b/test/test_local.sh @@ -0,0 +1,288 @@ +#!/usr/bin/env bash +# +# Executes the full test suite on the local host. +# +# WARNING!!! WARNING!!! WARNING!!! WARNING!!! WARNING!!! WARNING!!! WARNING!!! +# DO NOT RUN THIS SCRIPT ON A COMPUTER YOU AREN'T WILLING TO SACRIFICE. +# Running this script may result in undesired changes to this computer, such as: +# * installation of packages you don't want +# * broken or partial installations +# * broken build outputs overwriting working ones on the root filesystem +# * leaving files and data where you don't want them +# * messing with your shell and profiles +# * deleting files and directories wherever it wants to +# +# If you really want to sacrifice this computer, run with this env variable set: +# WRECK_THIS_COMPUTER="PLEASE_I_DESERVE_IT" + +echo "WARNING!!! WARNING!!! WARNING!!! WARNING!!! WARNING!!! WARNING!!! WARNING!!!" +echo "DO NOT RUN THIS SCRIPT ON A COMPUTER YOU AREN'T WILLING TO SACRIFICE." +echo "Running this script may result in undesired changes to this computer, such as:" +echo "* installation of packages you don't want" +echo "* broken or partial installations" +echo "* broken build outputs overwriting working ones on the root filesystem" +echo "* leaving files and data where you don't want them" +echo "* messing with your shell and profiles" +echo "* deleting files and directories wherever it wants to" +echo + +if [ "${WRECK_THIS_COMPUTER}" == "PLEASE_I_DESERVE_IT" ]; then + env | grep "WRECK_THIS_COMPUTER" + echo "Wrecking this computer, per request..." + echo + echo "Waiting five seconds..." + sleep 5 + echo +else + echo "If you really want to sacrifice this computer, run with this env variable set:" + echo 'WRECK_THIS_COMPUTER="PLEASE_I_DESERVE_IT"' + exit 1 +fi + +# Set this after the warning, so that the debug output doesn't confuse people +set -ex + +# set the debug flag for the installer script +export DEBUG=1 + +# Save path to the setup script, which is above this script. +SETUP_SCRIPT_PATH="$(pwd)/$(dirname "${0}")/../pico_setup.sh" + +backup() { + FILE="${1}" + if [ -f "${FILE}" ]; then + cp "${FILE}" "${FILE}.$(date +"%Y%m%d-%H%M%S")" + fi +} + +setup() { + backup ~/.profile + backup ~/.zprofile +} + +start_test() { + # Does the standard setup for a test + TEST_NAME="${1}" + echo "Starting ${TEST_NAME}" + + # Cleanse and change into test dir + TEST_DIR="/tmp/${TEST_NAME}" + rm -rf "${TEST_DIR}" || true + mkdir -p "${TEST_DIR}" + pushd "${TEST_DIR}" > /dev/null + + # should always start with this unset + unset TARGET_DIR +} + +execute_and_log() { + # Executes the given args, and captures output to a log file. + # Outputs the variable LOG_FILE, containing the path of file. + LOG_FILE="${TEST_DIR}/$(basename ${1}).$(date +"%Y%m%d-%H%M%S.%N").log" + echo execute_and_log: ${*} + echo logging to: ${LOG_FILE} + # Appending to this file is a bit of a gamble. The nanosecond-precision name should be unique, but in case it + # isn't, I'd rather append than lose data. + ${*} |& tee -a ${LOG_FILE} +} + +complete_test() { + popd > /dev/null + + echo "Completed ${TEST_NAME}" + unset TEST_NAME +} + +test_with_target_dir_unset() { + start_test test_with_target_dir_unset + + # just to be sure + unset TARGET_DIR + + execute_and_log "${SETUP_SCRIPT_PATH}" + + complete_test +} + +test_with_target_dir_set() { + start_test test_with_target_dir_set + + # Set the target dir to be something below the test dir + export TARGET_DIR="${TEST_DIR}/target_dir" + execute_and_log "${SETUP_SCRIPT_PATH}" + + # verify that at least one expected git repo exists + git -C "${TARGET_DIR}/pico-sdk" status + + unset TARGET_DIR + + complete_test +} + +test_update() { + start_test test_update + + # run the first time + execute_and_log "${SETUP_SCRIPT_PATH}" + + + # run a second time, watching for the indicator that we're doing the git pull. + execute_and_log "${SETUP_SCRIPT_PATH}" + COUNT=$(grep -c "git .* pull --ff-only" ${LOG_FILE}) + if [ 7 -ne "${COUNT}" ]; then + echo "Didn't see the expected number of git pulls" + exit 1 + fi + + complete_test +} + +test_with_space_in_target_dir() { + start_test test_with_space_in_target_dir + + # Set the target dir to be something below the test dir + export TARGET_DIR="${TEST_DIR}/target_dir_WITH SPACE" + execute_and_log "${SETUP_SCRIPT_PATH}" + + # verify that at least one expected git repo exists + git -C "${TARGET_DIR}/pico-sdk" status + + unset TARGET_DIR + + complete_test +} + +test_with_specific_shell() { + # Execute a test in the case that the user has a certain shell. Warning: this does not attempt to change the shell + # back. + # $1 must be the new shell + NEW_SHELL="${1}" + + # sudo to change shell, so it won't prompt for password + sudo chsh -s "${NEW_SHELL}" "${USER}" + + # try to verify that the user is, in fact, using the new shell: + sudo -i -u "${USER}" printenv SHELL | grep ${NEW_SHELL} + + execute_and_log sudo -i -u "${USER}" "${SETUP_SCRIPT_PATH}" +} + +test_with_bash_user() { + start_test test_with_bash_user + + # Setup the expected scenario + rm ~/.zprofile || true + cp /etc/skel/.profile ~ + + # This is a hack so that this test works. The problem is that the method of calling the installer doesn't + # pass the TARGET_DIR env var, so we really can't install it to anywhere in particular. It goes to the ~/pico + # whether you want it to or not. Setting the target dir with a command line switch should allow this to be fixed. + TARGET_DIR="${HOME}/pico" + # clean it out + rm -rf "${TARGET_DIR}" + + test_with_specific_shell "$(which bash)" + + # verify the change + test "$(tail -n 1 ~/.profile)" == "export \"PICO_SDK_PATH=${TARGET_DIR}/pico-sdk\"" + # and a non-change + test ! -f ~/.zprofile + + complete_test +} + +test_with_sh_user() { + start_test test_with_sh_user + + # Setup the expected scenario + rm ~/.zprofile || true + cp /etc/skel/.profile ~ + + # This is a hack so that this test works. The problem is that the method of calling the installer doesn't + # pass the TARGET_DIR env var, so we really can't install it to anywhere in particular. It goes to the ~/pico + # whether you want it to or not. Setting the target dir with a command line switch should allow this to be fixed. + TARGET_DIR="${HOME}/pico" + # clean it out + rm -rf "${TARGET_DIR}" + + test_with_specific_shell "$(which sh)" + + # verify the change + test "$(tail -n 1 ~/.profile)" == "export \"PICO_SDK_PATH=${TARGET_DIR}/pico-sdk\"" + # and a non-change + test ! -f ~/.zprofile + + complete_test +} + +test_with_zsh_user() { + start_test test_with_zsh_user + + # attempt to ensure zsh is installed + sudo apt -y install zsh + if [ ! -x "$(which zsh)" ]; then + echo "Can't find zsh" + exit 1 + fi + + # Setup the expected scenario + rm ~/.profile || true + rm ~/.zprofile || true + + # This is a hack so that this test works. The problem is that the method of calling the installer doesn't + # pass the TARGET_DIR env var, so we really can't install it to anywhere in particular. It goes to the ~/pico + # whether you want it to or not. Setting the target dir with a command line switch should allow this to be fixed. + TARGET_DIR="${HOME}/pico" + # clean it out + rm -rf "${TARGET_DIR}" + + test_with_specific_shell "$(which zsh)" + + # verify the change + test "$(tail -n 1 ~/.zprofile)" == "export \"PICO_SDK_PATH=${TARGET_DIR}/pico-sdk\"" + # and a non-change + test ! -f ~/.profile + + complete_test +} + +test_with_naked_tail_dot_profile() { + start_test test_with_naked_tail_dot_profile + + # Setup the expected scenario + echo -n "# no newline!" > ~/.profile + xxd ~/.profile + + # This is a hack so that this test works. The problem is that the method of calling the installer doesn't + # pass the TARGET_DIR env var, so we really can't install it to anywhere in particular. It goes to the ~/pico + # whether you want it to or not. Setting the target dir with a command line switch should allow this to be fixed. + TARGET_DIR="${HOME}/pico" + # clean it out + rm -rf "${TARGET_DIR}" + + test_with_specific_shell "$(which bash)" + + # verify the change + test "$(tail -n 1 ~/.profile)" == "export \"PICO_SDK_PATH=${TARGET_DIR}/pico-sdk\"" + + complete_test +} + +execute_all_tests() { + # Execute all the (enabled) tests + + setup + + test_with_target_dir_unset + test_with_target_dir_set + test_update + # Doesn't work because of jimtcl + # test_with_space_in_target_dir + test_with_bash_user + test_with_sh_user + test_with_zsh_user + test_with_naked_tail_dot_profile +} + + +execute_all_tests