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..56cf0df 100644 --- a/Makefile +++ b/Makefile @@ -22,6 +22,9 @@ endif @echo "deploy: build and upload to PyPi" @echo "tag: tag the repository with the current version\n" +version: + @hatch version + install: ./install.sh --unstable @@ -30,11 +33,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 @@ -44,7 +50,7 @@ pytest: nopost: @bash check.sh --nopost -tag: +tag: version git tag -a "v${LIBRARY_VERSION}" -m "Version ${LIBRARY_VERSION}" build: check diff --git a/README.md b/README.md index 97ec129..4459d0c 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # __TITLE__ [![Build Status](https://img.shields.io/github/actions/workflow/status/pimoroni/PROJECT_NAME-python/test.yml?branch=main)](https://github.com/pimoroni/PROJECT_NAME-python/actions/workflows/test.yml) -[![Coverage Status](https://coveralls.io/repos/github/pimoroni/PROJECT_NAME-python/badge.svg?branch=master)](https://coveralls.io/github/pimoroni/PROJECT_NAME-python?branch=master) +[![Coverage Status](https://coveralls.io/repos/github/pimoroni/PROJECT_NAME-python/badge.svg?branch=main)](https://coveralls.io/github/pimoroni/PROJECT_NAME-python?branch=main) [![PyPi Package](https://img.shields.io/pypi/v/PROJECT_NAME.svg)](https://pypi.python.org/pypi/PROJECT_NAME) [![Python Versions](https://img.shields.io/pypi/pyversions/PROJECT_NAME.svg)](https://pypi.python.org/pypi/PROJECT_NAME) 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 b45e7c8..3db90bc 100755 --- a/install.sh +++ b/install.sh @@ -1,22 +1,24 @@ #!/bin/bash -LIBRARY_NAME=`grep -m 1 name pyproject.toml | awk -F" = " '{print substr($2,2,length($2)-2)}'` -CONFIG=/boot/config.txt -DATESTAMP=`date "+%Y-%m-%d-%H-%M-%S"` +LIBRARY_NAME=$(grep -m 1 name pyproject.toml | awk -F" = " '{print substr($2,2,length($2)-2)}') +CONFIG_FILE=config.txt +CONFIG_DIR="/boot/firmware" +DATESTAMP=$(date "+%Y-%m-%d-%H-%M-%S") CONFIG_BACKUP=false APT_HAS_UPDATED=false -RESOURCES_TOP_DIR=$HOME/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="/usr/bin/python3" +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 } @@ -33,15 +35,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)" } @@ -51,51 +44,126 @@ 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 + fatal "Could not find $CONFIG_FILE!" + fi + fi + inform "Using $CONFIG_FILE in $CONFIG_DIR" +} + +venv_bash_snippet() { + inform "Checking for $VENV_BASH_SNIPPET\n" + if [ ! -f "$VENV_BASH_SNIPPET" ]; then + inform "Creating $VENV_BASH_SNIPPET\n" + 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" +if [ ! -f \$VENV_DIR/bin/activate ]; then + printf "Creating user Python environment in \$VENV_DIR, please wait...\n" + mkdir -p \$VENV_DIR + python3 -m venv --system-site-packages \$VENV_DIR +fi +printf " ↓ ↓ ↓ ↓ Hello, we've activated a Python venv for you. To exit, type \"deactivate\".\n" +source \$VENV_DIR/bin/activate +EOF + fi +} + +venv_check() { + 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 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 "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 + else + 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 { if [ ! $CONFIG_BACKUP == true ]; then CONFIG_BACKUP=true FILENAME="config.preinstall-$LIBRARY_NAME-$DATESTAMP.txt" - inform "Backing up $CONFIG to /boot/$FILENAME\n" - sudo cp $CONFIG /boot/$FILENAME - mkdir -p $RESOURCES_TOP_DIR/config-backups/ - cp $CONFIG $RESOURCES_TOP_DIR/config-backups/$FILENAME + 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" if [ -f "$UNINSTALLER" ]; then - echo "cp $RESOURCES_TOP_DIR/config-backups/$FILENAME $CONFIG" >> $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 + # shellcheck disable=SC2086 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 @@ -116,8 +184,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") @@ -125,119 +193,155 @@ while [[ $# -gt 0 ]]; do esac done +printf "Installing %s...\n\n" "$LIBRARY_NAME" + user_check +venv_check -if [ ! -f "$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" 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 /boot/config.txt and trigger a backup - if [[ "$CMD" == *"raspi-config"* ]] || [[ "$CMD" == *"$CONFIG"* ]] || [[ "$CMD" == *"\$CONFIG"* ]]; then + # 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\n" - sudo sed -i "s/^#$CONFIG_LINE/$CONFIG_LINE/" $CONFIG - if ! grep -q "^$CONFIG_LINE" $CONFIG; then - printf "$CONFIG_LINE\n" | sudo tee --append $CONFIG + 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 "%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 $LIBRARY_NAME -o $RESOURCES_DIR/docs > /dev/null - if [ $? -eq 0 ]; then + inform "Generating documentation.\n" + if $PYTHON -m pdoc "$LIBRARY_NAME" -o "$RESOURCES_DIR/docs" > /dev/null; then inform "Documentation saved to $RESOURCES_DIR/docs" success "Done!" else @@ -245,6 +349,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/pyproject.toml b/pyproject.toml index d80e486..c2a6186 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -107,7 +107,7 @@ ignore = [ 'requirements-dev.txt' ] -[pimoroni] -apt_packages = ["python3-rpi.gpio", "python3-smbus"] +[tool.pimoroni] +apt_packages = ["git"] configtxt = [] -commands = [] +commands = ["false"] diff --git a/tox.ini b/tox.ini index 090fdb0..4726cef 100644 --- a/tox.ini +++ b/tox.ini @@ -20,7 +20,7 @@ commands = python -m build --no-isolation python -m twine check dist/* isort --check . - ruff --format=github . + ruff check . codespell . deps = check-manifest diff --git a/uninstall.sh b/uninstall.sh index e013945..3314b7f 100755 --- a/uninstall.sh +++ b/uninstall.sh @@ -1,12 +1,21 @@ #!/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="/usr/bin/python3" +PYTHON="python" + + +venv_check() { + 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 + fi +} 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 @@ -46,16 +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