From a2bab378bae282c98006bb834e3ddaae963bb016 Mon Sep 17 00:00:00 2001 From: Phil Howard Date: Thu, 11 Jan 2024 13:56:20 +0000 Subject: [PATCH 1/2] Sync with latest boilerplate. * CI: Update GitHub Action versions. * QA: Add shellcheck and fix/ignore all issues. * install.sh: slightly better feedback for setup commands. * install.sh: fix quoting bug in do_config_backup. * install.sh: don't output printf commands. --- .github/workflows/build.yml | 6 +- .github/workflows/qa.yml | 9 +- .github/workflows/test.yml | 2 +- Makefile | 5 +- check.sh | 21 ++-- install.sh | 231 ++++++++++++++++++++++-------------- uninstall.sh | 14 +-- 7 files changed, 175 insertions(+), 113 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 87200ef..07620e3 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -19,10 +19,10 @@ jobs: steps: - name: Checkout Code - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python }} - uses: actions/setup-python@v3 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python }} @@ -35,7 +35,7 @@ jobs: make build - name: Upload Packages - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: ${{ env.RELEASE_FILE }} path: dist/ diff --git a/.github/workflows/qa.yml b/.github/workflows/qa.yml index 4f85883..ac672a5 100644 --- a/.github/workflows/qa.yml +++ b/.github/workflows/qa.yml @@ -10,16 +10,15 @@ jobs: test: name: linting & spelling runs-on: ubuntu-latest - env: TERM: xterm-256color steps: - name: Checkout Code - uses: actions/checkout@v2 + uses: actions/checkout@v4 - name: Set up Python '3,11' - uses: actions/setup-python@v3 + uses: actions/setup-python@v5 with: python-version: '3.11' @@ -34,3 +33,7 @@ jobs: - name: Run Code Checks run: | make check + + - name: Run Bash Code Checks + run: | + make shellcheck diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 016a678..6f8cff7 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -19,7 +19,7 @@ jobs: uses: actions/checkout@v3 - name: Set up Python ${{ matrix.python }} - uses: actions/setup-python@v3 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python }} diff --git a/Makefile b/Makefile index 9e0c15c..34f4a7d 100644 --- a/Makefile +++ b/Makefile @@ -30,11 +30,14 @@ uninstall: dev-deps: python3 -m pip install -r requirements-dev.txt - sudo apt install dos2unix + sudo apt install dos2unix shellcheck check: @bash check.sh +shellcheck: + shellcheck *.sh + qa: tox -e qa diff --git a/check.sh b/check.sh index cbb1565..38dfc3a 100755 --- a/check.sh +++ b/check.sh @@ -3,9 +3,10 @@ # This script handles some basic QA checks on the source NOPOST=$1 -LIBRARY_NAME=`hatch project metadata name` -LIBRARY_VERSION=`hatch version | awk -F "." '{print $1"."$2"."$3}'` -POST_VERSION=`hatch version | awk -F "." '{print substr($4,0,length($4))}'` +LIBRARY_NAME=$(hatch project metadata name) +LIBRARY_VERSION=$(hatch version | awk -F "." '{print $1"."$2"."$3}') +POST_VERSION=$(hatch version | awk -F "." '{print substr($4,0,length($4))}') +TERM=${TERM:="xterm-256color"} success() { echo -e "$(tput setaf 2)$1$(tput sgr0)" @@ -28,7 +29,7 @@ while [[ $# -gt 0 ]]; do ;; *) if [[ $1 == -* ]]; then - printf "Unrecognised option: $1\n"; + printf "Unrecognised option: %s\n" "$1"; exit 1 fi POSITIONAL_ARGS+=("$1") @@ -39,8 +40,7 @@ done inform "Checking $LIBRARY_NAME $LIBRARY_VERSION\n" inform "Checking for trailing whitespace..." -grep -IUrn --color "[[:blank:]]$" --exclude-dir=dist --exclude-dir=.tox --exclude-dir=.git --exclude=PKG-INFO -if [[ $? -eq 0 ]]; then +if grep -IUrn --color "[[:blank:]]$" --exclude-dir=dist --exclude-dir=.tox --exclude-dir=.git --exclude=PKG-INFO; then warning "Trailing whitespace found!" exit 1 else @@ -49,8 +49,7 @@ fi printf "\n" inform "Checking for DOS line-endings..." -grep -lIUrn --color $'\r' --exclude-dir=dist --exclude-dir=.tox --exclude-dir=.git --exclude=Makefile -if [[ $? -eq 0 ]]; then +if grep -lIUrn --color $'\r' --exclude-dir=dist --exclude-dir=.tox --exclude-dir=.git --exclude=Makefile; then warning "DOS line-endings found!" exit 1 else @@ -59,8 +58,7 @@ fi printf "\n" inform "Checking CHANGELOG.md..." -cat CHANGELOG.md | grep ^${LIBRARY_VERSION} > /dev/null 2>&1 -if [[ $? -eq 1 ]]; then +if ! grep "^${LIBRARY_VERSION}" CHANGELOG.md > /dev/null 2>&1; then warning "Changes missing for version ${LIBRARY_VERSION}! Please update CHANGELOG.md." exit 1 else @@ -69,8 +67,7 @@ fi printf "\n" inform "Checking for git tag ${LIBRARY_VERSION}..." -git tag -l | grep -E "${LIBRARY_VERSION}$" -if [[ $? -eq 1 ]]; then +if ! git tag -l | grep -E "${LIBRARY_VERSION}$"; then warning "Missing git tag for version ${LIBRARY_VERSION}" fi printf "\n" diff --git a/install.sh b/install.sh index 797954e..8ca68fa 100755 --- a/install.sh +++ b/install.sh @@ -1,26 +1,25 @@ #!/bin/bash -LIBRARY_NAME=`grep -m 1 name pyproject.toml | awk -F" = " '{print substr($2,2,length($2)-2)}'` -MODULE_NAME="bme280" +LIBRARY_NAME=$(grep -m 1 name pyproject.toml | awk -F" = " '{print substr($2,2,length($2)-2)}') +MODULE_NAME="grow" CONFIG_FILE=config.txt CONFIG_DIR="/boot/firmware" -DATESTAMP=`date "+%Y-%m-%d-%H-%M-%S"` +DATESTAMP=$(date "+%Y-%m-%d-%H-%M-%S") CONFIG_BACKUP=false APT_HAS_UPDATED=false -RESOURCES_TOP_DIR=$HOME/Pimoroni -VENV_BASH_SNIPPET=$RESOURCES_TOP_DIR/auto_venv.sh -VENV_DIR=$HOME/.virtualenvs/pimoroni -WD=`pwd` +RESOURCES_TOP_DIR="$HOME/Pimoroni" +VENV_BASH_SNIPPET="$RESOURCES_TOP_DIR/auto_venv.sh" +VENV_DIR="$HOME/.virtualenvs/pimoroni" USAGE="./install.sh (--unstable)" POSITIONAL_ARGS=() FORCE=false UNSTABLE=false PYTHON="python" +CMD_ERRORS=false user_check() { - if [ $(id -u) -eq 0 ]; then - printf "Script should not be run as root. Try './install.sh'\n" - exit 1 + if [ "$(id -u)" -eq 0 ]; then + fatal "Script should not be run as root. Try './install.sh'\n" fi } @@ -37,15 +36,6 @@ confirm() { fi } -prompt() { - read -r -p "$1 [y/N] " response < /dev/tty - if [[ $response =~ ^(yes|y|Y)$ ]]; then - true - else - false - fi -} - success() { echo -e "$(tput setaf 2)$1$(tput sgr0)" } @@ -55,15 +45,19 @@ inform() { } warning() { - echo -e "$(tput setaf 1)$1$(tput sgr0)" + echo -e "$(tput setaf 1)⚠ WARNING:$(tput sgr0) $1" +} + +fatal() { + echo -e "$(tput setaf 1)⚠ FATAL:$(tput sgr0) $1" + exit 1 } find_config() { if [ ! -f "$CONFIG_DIR/$CONFIG_FILE" ]; then CONFIG_DIR="/boot" - if [ ! -f "$CONFIG_DIR/$CONFIG_FILE"]; then - warning "Could not find $CONFIG_FILE!" - exit 1 + if [ ! -f "$CONFIG_DIR/$CONFIG_FILE" ]; then + fatal "Could not find $CONFIG_FILE!" fi else if [ -f "/boot/$CONFIG_FILE" ] && [ ! -L "/boot/$CONFIG_FILE" ]; then @@ -76,9 +70,10 @@ find_config() { venv_bash_snippet() { inform "Checking for $VENV_BASH_SNIPPET\n" - if [ ! -f $VENV_BASH_SNIPPET ]; then + if [ ! -f "$VENV_BASH_SNIPPET" ]; then inform "Creating $VENV_BASH_SNIPPET\n" - cat << EOF > $VENV_BASH_SNIPPET + mkdir -p "$RESOURCES_TOP_DIR" + cat << EOF > "$VENV_BASH_SNIPPET" # Add "source $VENV_BASH_SNIPPET" to your ~/.bashrc to activate # the Pimoroni virtual environment automagically! VENV_DIR="$VENV_DIR" @@ -94,26 +89,37 @@ EOF } venv_check() { - PYTHON_BIN=`which $PYTHON` + PYTHON_BIN=$(which "$PYTHON") if [[ $VIRTUAL_ENV == "" ]] || [[ $PYTHON_BIN != $VIRTUAL_ENV* ]]; then printf "This script should be run in a virtual Python environment.\n" - if confirm "Would you like us to create one for you?"; then - if [ ! -f $VENV_DIR/bin/activate ]; then - inform "Creating virtual Python environment in $VENV_DIR, please wait...\n" - mkdir -p $VENV_DIR - /usr/bin/python3 -m venv $VENV_DIR --system-site-packages + if confirm "Would you like us to create and/or use a default one?"; then + printf "\n" + if [ ! -f "$VENV_DIR/bin/activate" ]; then + inform "Creating a new virtual Python environment in $VENV_DIR, please wait...\n" + mkdir -p "$VENV_DIR" + /usr/bin/python3 -m venv "$VENV_DIR" --system-site-packages venv_bash_snippet + # shellcheck disable=SC1091 + source "$VENV_DIR/bin/activate" else - inform "Found existing virtual Python environment in $VENV_DIR\n" + inform "Activating existing virtual Python environment in $VENV_DIR\n" + printf "source \"%s/bin/activate\"\n" "$VENV_DIR" + # shellcheck disable=SC1091 + source "$VENV_DIR/bin/activate" fi - inform "Activating virtual Python environment in $VENV_DIR..." - inform "source $VENV_DIR/bin/activate\n" - source $VENV_DIR/bin/activate - else - exit 1 + printf "\n" + fatal "Please create and/or activate a virtual Python environment and try again!\n" fi fi + printf "\n" +} + +check_for_error() { + if [ $? -ne 0 ]; then + CMD_ERRORS=true + warning "^^^ 😬 previous command did not exit cleanly!" + fi } function do_config_backup { @@ -121,43 +127,48 @@ function do_config_backup { CONFIG_BACKUP=true FILENAME="config.preinstall-$LIBRARY_NAME-$DATESTAMP.txt" inform "Backing up $CONFIG_DIR/$CONFIG_FILE to $CONFIG_DIR/$FILENAME\n" - sudo cp $CONFIG_DIR/$CONFIG_FILE $CONFIG_DIR/$FILENAME - mkdir -p $RESOURCES_TOP_DIR/config-backups/ - cp $CONFIG_DIR/$CONFIG_FILE $RESOURCES_TOP_DIR/config-backups/$FILENAME + sudo cp "$CONFIG_DIR/$CONFIG_FILE" "$CONFIG_DIR/$FILENAME" + mkdir -p "$RESOURCES_TOP_DIR/config-backups/" + cp $CONFIG_DIR/$CONFIG_FILE "$RESOURCES_TOP_DIR/config-backups/$FILENAME" if [ -f "$UNINSTALLER" ]; then - echo "cp $RESOURCES_TOP_DIR/config-backups/$FILENAME $CONFIG_DIR/$CONFIG_FILE" >> $UNINSTALLER + echo "cp $RESOURCES_TOP_DIR/config-backups/$FILENAME $CONFIG_DIR/$CONFIG_FILE" >> "$UNINSTALLER" fi fi } function apt_pkg_install { - PACKAGES=() + PACKAGES_NEEDED=() PACKAGES_IN=("$@") + # Check the list of packages and only run update/install if we need to for ((i = 0; i < ${#PACKAGES_IN[@]}; i++)); do PACKAGE="${PACKAGES_IN[$i]}" if [ "$PACKAGE" == "" ]; then continue; fi - printf "Checking for $PACKAGE\n" - dpkg -L $PACKAGE > /dev/null 2>&1 + printf "Checking for %s\n" "$PACKAGE" + dpkg -L "$PACKAGE" > /dev/null 2>&1 if [ "$?" == "1" ]; then - PACKAGES+=("$PACKAGE") + PACKAGES_NEEDED+=("$PACKAGE") fi done - PACKAGES="${PACKAGES[@]}" + PACKAGES="${PACKAGES_NEEDED[*]}" if ! [ "$PACKAGES" == "" ]; then - echo "Installing missing packages: $PACKAGES" + printf "\n" + inform "Installing missing packages: $PACKAGES" if [ ! $APT_HAS_UPDATED ]; then sudo apt update APT_HAS_UPDATED=true fi - sudo apt install -y $PACKAGES + sudo apt install -y "$PACKAGES" + check_for_error if [ -f "$UNINSTALLER" ]; then - echo "apt uninstall -y $PACKAGES" >> $UNINSTALLER + echo "apt uninstall -y $PACKAGES" >> "$UNINSTALLER" fi fi } function pip_pkg_install { + # A null Keyring prevents pip stalling in the background PYTHON_KEYRING_BACKEND=keyring.backends.null.Keyring $PYTHON -m pip install --upgrade "$@" + check_for_error } while [[ $# -gt 0 ]]; do @@ -178,8 +189,8 @@ while [[ $# -gt 0 ]]; do ;; *) if [[ $1 == -* ]]; then - printf "Unrecognised option: $1\n"; - printf "Usage: $USAGE\n"; + printf "Unrecognised option: %s\n" "$1"; + printf "Usage: %s\n" "$USAGE"; exit 1 fi POSITIONAL_ARGS+=("$1") @@ -187,57 +198,67 @@ while [[ $# -gt 0 ]]; do esac done +printf "Installing %s...\n\n" "$LIBRARY_NAME" + user_check venv_check -if [ ! -f `which $PYTHON` ]; then - printf "Python path $PYTHON not found!\n" - exit 1 +if [ ! -f "$(which "$PYTHON")" ]; then + fatal "Python path %s not found!\n" "$PYTHON" fi -PYTHON_VER=`$PYTHON --version` - -printf "$LIBRARY_NAME Python Library: Installer\n\n" +PYTHON_VER=$($PYTHON --version) inform "Checking Dependencies. Please wait..." +# Install toml and try to read pyproject.toml into bash variables + pip_pkg_install toml -CONFIG_VARS=`$PYTHON - < $UNINSTALLER +# Create a stub uninstaller file, we'll try to add the inverse of every +# install command run to here, though it's not complete. +cat << EOF > "$UNINSTALLER" printf "It's recommended you run these steps manually.\n" printf "If you want to run the full script, open it in\n" printf "an editor and remove 'exit 1' from below.\n" @@ -245,65 +266,87 @@ exit 1 source $VIRTUAL_ENV/bin/activate EOF -if $UNSTABLE; then - warning "Installing unstable library from source.\n\n" -else - printf "Installing stable library from pypi.\n\n" -fi +printf "\n" inform "Installing for $PYTHON_VER...\n" + +# Install apt packages from pyproject.toml / tool.pimoroni.apt_packages apt_pkg_install "${APT_PACKAGES[@]}" + +printf "\n" + if $UNSTABLE; then + warning "Installing unstable library from source.\n" pip_pkg_install . else - pip_pkg_install $LIBRARY_NAME + inform "Installing stable library from pypi.\n" + pip_pkg_install "$LIBRARY_NAME" fi + +# shellcheck disable=SC2181 # One of two commands run, depending on --unstable flag if [ $? -eq 0 ]; then success "Done!\n" - echo "$PYTHON -m pip uninstall $LIBRARY_NAME" >> $UNINSTALLER + echo "$PYTHON -m pip uninstall $LIBRARY_NAME" >> "$UNINSTALLER" fi -cd $WD - find_config +printf "\n" + +# Run the setup commands from pyproject.toml / tool.pimoroni.commands + +inform "Running setup commands...\n" for ((i = 0; i < ${#SETUP_CMDS[@]}; i++)); do CMD="${SETUP_CMDS[$i]}" # Attempt to catch anything that touches config.txt and trigger a backup if [[ "$CMD" == *"raspi-config"* ]] || [[ "$CMD" == *"$CONFIG_DIR/$CONFIG_FILE"* ]] || [[ "$CMD" == *"\$CONFIG_DIR/\$CONFIG_FILE"* ]]; then do_config_backup fi - eval $CMD + if [[ ! "$CMD" == printf* ]]; then + printf "Running: \"%s\"\n" "$CMD" + fi + eval "$CMD" + check_for_error done +printf "\n" + +# Add the config.txt entries from pyproject.toml / tool.pimoroni.configtxt + for ((i = 0; i < ${#CONFIG_TXT[@]}; i++)); do CONFIG_LINE="${CONFIG_TXT[$i]}" if ! [ "$CONFIG_LINE" == "" ]; then do_config_backup - inform "Adding $CONFIG_LINE to $CONFIG_DIR/$CONFIG_FILE\n" + inform "Adding $CONFIG_LINE to $CONFIG_DIR/$CONFIG_FILE" sudo sed -i "s/^#$CONFIG_LINE/$CONFIG_LINE/" $CONFIG_DIR/$CONFIG_FILE if ! grep -q "^$CONFIG_LINE" $CONFIG_DIR/$CONFIG_FILE; then - printf "$CONFIG_LINE\n" | sudo tee --append $CONFIG_DIR/$CONFIG_FILE + printf "%s \n" "$CONFIG_LINE" | sudo tee --append $CONFIG_DIR/$CONFIG_FILE fi fi done +printf "\n" + +# Just a straight copy of the examples/ dir into ~/Pimoroni/board/examples + if [ -d "examples" ]; then if confirm "Would you like to copy examples to $RESOURCES_DIR?"; then inform "Copying examples to $RESOURCES_DIR" - cp -r examples/ $RESOURCES_DIR - echo "rm -r $RESOURCES_DIR" >> $UNINSTALLER + cp -r examples/ "$RESOURCES_DIR" + echo "rm -r $RESOURCES_DIR" >> "$UNINSTALLER" success "Done!" fi fi printf "\n" +# Use pdoc to generate basic documentation from the installed module + if confirm "Would you like to generate documentation?"; then + inform "Installing pdoc. Please wait..." pip_pkg_install pdoc - printf "Generating documentation.\n" - $PYTHON -m pdoc $MODULE_NAME -o $RESOURCES_DIR/docs > /dev/null - if [ $? -eq 0 ]; then + inform "Generating documentation.\n" + if $PYTHON -m pdoc "$MODULE_NAME" -o "$RESOURCES_DIR/docs" > /dev/null; then inform "Documentation saved to $RESOURCES_DIR/docs" success "Done!" else @@ -311,6 +354,22 @@ if confirm "Would you like to generate documentation?"; then fi fi -success "\nAll done!" -inform "If this is your first time installing you should reboot for hardware changes to take effect.\n" -inform "Find uninstall steps in $UNINSTALLER\n" +printf "\n" + +if [ "$CMD_ERRORS" = true ]; then + warning "One or more setup commands appear to have failed." + printf "This might prevent things from working properly.\n" + printf "Make sure your OS is up to date and try re-running this installer.\n" + printf "If things still don't work, report this or find help at %s.\n\n" "$GITHUB_URL" +else + success "\nAll done!" +fi + +printf "If this is your first time installing you should reboot for hardware changes to take effect.\n" +printf "Find uninstall steps in %s\n\n" "$UNINSTALLER" + +if [ "$CMD_ERRORS" = true ]; then + exit 1 +else + exit 0 +fi diff --git a/uninstall.sh b/uninstall.sh index f213fc5..3314b7f 100755 --- a/uninstall.sh +++ b/uninstall.sh @@ -1,13 +1,13 @@ #!/bin/bash FORCE=false -LIBRARY_NAME=`grep -m 1 name pyproject.toml | awk -F" = " '{print substr($2,2,length($2)-2)}'` +LIBRARY_NAME=$(grep -m 1 name pyproject.toml | awk -F" = " '{print substr($2,2,length($2)-2)}') RESOURCES_DIR=$HOME/Pimoroni/$LIBRARY_NAME PYTHON="python" venv_check() { - PYTHON_BIN=`which $PYTHON` + PYTHON_BIN=$(which $PYTHON) if [[ $VIRTUAL_ENV == "" ]] || [[ $PYTHON_BIN != $VIRTUAL_ENV* ]]; then printf "This script should be run in a virtual Python environment.\n" exit 1 @@ -15,7 +15,7 @@ venv_check() { } user_check() { - if [ $(id -u) -eq 0 ]; then + if [ "$(id -u)" -eq 0 ]; then printf "Script should not be run as root. Try './uninstall.sh'\n" exit 1 fi @@ -55,17 +55,17 @@ warning() { echo -e "$(tput setaf 1)$1$(tput sgr0)" } -printf "$LIBRARY_NAME Python Library: Uninstaller\n\n" +printf "%s Python Library: Uninstaller\n\n" "$LIBRARY_NAME" user_check venv_check printf "Uninstalling for Python 3...\n" -$PYTHON -m pip uninstall $LIBRARY_NAME +$PYTHON -m pip uninstall "$LIBRARY_NAME" -if [ -d $RESOURCES_DIR ]; then +if [ -d "$RESOURCES_DIR" ]; then if confirm "Would you like to delete $RESOURCES_DIR?"; then - rm -r $RESOURCES_DIR + rm -r "$RESOURCES_DIR" fi fi From 333f5627e3e2c34014ab03ef350a539304fa3dbe Mon Sep 17 00:00:00 2001 From: Phil Howard Date: Mon, 19 Feb 2024 13:36:20 +0000 Subject: [PATCH 2/2] install.sh: Set correct MODULE_NAME. --- install.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install.sh b/install.sh index 8ca68fa..02dd96d 100755 --- a/install.sh +++ b/install.sh @@ -1,6 +1,6 @@ #!/bin/bash LIBRARY_NAME=$(grep -m 1 name pyproject.toml | awk -F" = " '{print substr($2,2,length($2)-2)}') -MODULE_NAME="grow" +MODULE_NAME="bme280" CONFIG_FILE=config.txt CONFIG_DIR="/boot/firmware" DATESTAMP=$(date "+%Y-%m-%d-%H-%M-%S")