name: 'Setup Common Lisp' author: Alexander Artemenko description: This action setup Roswell and a Common Lisp implementation plus Qlot for managing virtual environments. inputs: roswell-version: description: 'Roswell version to install. If not specified, the latest working version will be used; if "latest", the latest version is used' required: false default: v23.10.14.114 asdf-system: description: 'ASDF system to install' required: false asdf-version: description: 'ASDF version to install. If not specified, the latest working version will be used; if "latest", the latest version is used' required: false default: 3.3.5.3 qlot-version: description: 'Qlot version to install. If not specified, the latest working version will be used; if "latest", the latest version is used' required: false default: 0.11.5 qlot-no-deps: description: 'Make Qlot ignore asd files and their dependencies. This this might be useful in rare cases when your project contains a non-readable asd files such as templates.' required: false default: false qlfile-template: description: "Djula template for qlfile. All environment variables are available in it's context" required: false cache: description: 'If true (default), then cache will be created to speedup repeated action runs.' required: false default: true # GitHub does not support anchors in the action # and returns error like this: # # Anchors are not currently supported. Remove the anchor 'roswell-cache-paths' # # That is why I use "input" variable to not repeat this list in two places roswell-cache-paths: description: "Internal var. Don't use it." required: false default: | ~/.quicklisp-client-fix ~/.roswell /usr/local/etc/roswell /usr/local/bin/ros /usr/local/Cellar/roswell /opt/homebrew/bin/ros /opt/homebrew/Cellar/roswell qlot-cache-paths: description: "Internal var. Don't use it." required: false default: | path: | qlfile qlfile.lock ~/.cache/common-lisp/ .qlot cache-suffix: description: "Internal var. Don't use it." required: false default: v9 runs: using: composite steps: - name: Calculate variables id: locals shell: bash run: | if [[ '${{ inputs.roswell-version }}' == 'latest' ]] then echo "windows-package-name=mingw-w64-x86_64-roswell" >> $GITHUB_OUTPUT else # Strip v prefix from version number ROS_VERSION=$(echo ${{ inputs.roswell-version }} | sed 's/v//') echo "windows-package-name=mingw-w64-x86_64-roswell=$ROS_VERSION" >> $GITHUB_OUTPUT fi # echo 'roswell-cache-paths=~/.quicklisp-client-fix\n~/.roswell\n/usr/local/etc/roswell\n/usr/local/bin/ros\n/usr/local/Cellar/roswell' >> $GITHUB_OUTPUT echo "current-month=$(date -u '+%Y-%m')" >> $GITHUB_OUTPUT - if: runner.os == 'Windows' uses: msys2/setup-msys2@cc11e9188b693c2b100158c3322424c4cc1dadea #v2.22.0 with: # Msys2 has its own PATH, and the following setting enables standard # PATH manipulation expressions like the one shown below, to succeed: # # $ echo /usr/local/bin >> $GITHUB_PATH path-type: inherit platform-check-severity: warn # Installing ASDF requires `make`, so let's make sure it's # available install: >- make ${{ steps.locals.outputs.windows-package-name }} cache: ${{ inputs.cache }} - name: Create lispsh shell: bash run: | echo ::group::Set up link to lispsh # All the steps below, should work without problems on Linux, Mac OS, # and Windows, provided that they are run with the "right" shell # parameter, i.e. bash for Linux and Mac OS, and msys2 for Windows. # # Unfortunately, composite actions do not support getting the shell # parameter injected from the parent workflow (read more about this # here: https://github.com/actions/runner/issues/835), so the # workaround I came up with is: # # 1. Symlink bash/msys2 to a known location, i.e. lispsh # 2. Use lispsh -eo pipefail {0} as shell parameter # # Pay attention to -eo pipefail options. We need them to exit on the # first error. Without this option, Roswell might fail to install the # implementation and continue to execute everything with default SBCL. # # It's not ideal, but the alternative is to duplicate most of the steps # below, and have some of them with `shell: bash`, and others with # `shell: msys2 {0}`. if [[ "$RUNNER_OS" == "Windows" ]]; then powershell New-Item -ItemType SymbolicLink -Force \ -Path "D:/a/_temp/setup-msys2/lispsh.cmd" \ -Target "D:/a/_temp/setup-msys2/msys2.cmd" else sudo ln -sf $(which bash) /usr/local/bin/lispsh fi echo ::endgroup:: - name: Set up Environment shell: bash run: | echo ::group::Set up Environment if [[ "$RUNNER_OS" == "Windows" ]]; then # Roswell internally checks for the MSYSCON env varible to be # defined, and when not there, it would go and install msys2 (i.e. # `ros install msys2+`) and rely on the `bash` bonary that comes # with that installation. # # All good except that something is not quite working as it should, # given that every time Roswell tries to run a `bash` command, it # would spit out the following: # # Unhandled SIMPLE-ERROR in thread #: # Couldn't execute "C:\\Users\\runneradmin\\.roswell\\impls\\x86-64\\windows\\msys2\\NIL\\usr\\bin\\bash": The system cannot find the file specified. # # Backtrace for: # # 0: (SB-DEBUG::DEBUGGER-DISABLED-HOOK # # :QUIT T) # 1: (SB-DEBUG::RUN-HOOK SB-EXT:*INVOKE-DEBUGGER-HOOK* #) # 2: (INVOKE-DEBUGGER #) # 3: (ERROR "Couldn't execute ~S: ~A" "C:\\Users\\runneradmin\\.roswell\\impls\\x86-64\\windows\\msys2\\NIL\\usr\\bin\\bash" "The system cannot find the file specified.") # 4: (SB-EXT:RUN-PROGRAM "C:\\Users\\runneradmin\\.roswell\\impls\\x86-64\\windows\\msys2\\NIL\\usr\\bin\\bash" ("-lc" "cd \"C:\\\\Users\\\\runneradmin\\\\.roswell\\\\src\\\\asdf-3.3.5.3\\\\\";pwd") :ENV NIL :ENVIRONMENT NIL :WAIT NIL :SEARCH T :INPUT NIL :IF-INPUT-DOES-NOT-EXIST :ERROR :OUTPUT :STREAM :IF-OUTPUT-EXISTS :APPEND :ERROR NIL :IF-ERROR-EXISTS :APPEND :STATUS-HOOK NIL :EXTERNAL-FORMAT :UTF-8 :DIRECTORY NIL :PRESERVE-FDS NIL :ESCAPE-ARGUMENTS T :WINDOW NIL) # 5: (UIOP/LAUNCH-PROGRAM:LAUNCH-PROGRAM ("C:\\Users\\runneradmin\\.roswell\\impls\\x86-64\\windows\\msys2\\NIL\\usr\\bin\\bash" "-lc" "cd \"C:\\\\Users\\\\runneradmin\\\\.roswell\\\\src\\\\asdf-3.3.5.3\\\\\";pwd") :INPUT NIL :OUTPUT :STREAM :ERROR-OUTPUT NIL :OUTPUT :STRING) # 6: ((LAMBDA (UIOP/RUN-PROGRAM::REDUCED-INPUT UIOP/RUN-PROGRAM::INPUT-ACTIVITY) :IN UIOP/RUN-PROGRAM::%USE-LAUNCH-PROGRAM) NIL NIL) # 7: (UIOP/RUN-PROGRAM::%USE-LAUNCH-PROGRAM ("C:\\Users\\runneradmin\\.roswell\\impls\\x86-64\\windows\\msys2\\NIL\\usr\\bin\\bash" "-lc" "cd \"C:\\\\Users\\\\runneradmin\\\\.roswell\\\\src\\\\asdf-3.3.5.3\\\\\";pwd") :OUTPUT :STRING) # 8: (MINGW-NAMESTRING #P"C:/Users/runneradmin/.roswell/src/asdf-3.3.5.3/") # 9: (ROSWELL.INSTALL.ASDF::ASDF-INSTALL (:TARGET "asdf" :VERSION "3.3.5.3" :VERSION-NOT-SPECIFIED 0 :ARGV NIL)) # # The NIL over there, seems to be the result of evaluating the # following form: # # (config "msys2.version") # # Now, I am not sure what's going on with that, but since # we got msys2 installed already, I figured it would be easier to # tell Roswell about it and ignore all the other installation # steps. echo MSYSCON=Stop-Roswell-From-Installing-Msys2 >> $GITHUB_ENV # Also, for whatever reason Roswell seems to be installing # ASDF-system-specific scripts inside .roswell/lisp/quicklisp/bin # and not .roswell/bin, so if we want to enable users of this # action to directly invoke these scripts, we need to add # .roswell/lisp/quicklisp/bin to PATH. echo $HOME/.roswell/lisp/quicklisp/bin >> $GITHUB_PATH fi echo $HOME/.roswell/bin >> $GITHUB_PATH echo ::endgroup:: # TODO: comment for prod # - name: Current Env # shell: bash # run: | # echo ::group::Environment # echo "Current dir:" # pwd # echo "Environment Variables:" # env | sort -u # echo ::endgroup:: # On Windows we dont have such problems with permission. # Also we don't have sudo there, so just skip this step # on this platform: - if: inputs.cache == 'true' && runner.os != 'Windows' name: Grant All Perms to Make Roswell Cache Restoring Possible shell: lispsh -eo pipefail {0} run: | sudo mkdir -p /usr/local/etc/roswell sudo chown "${USER}" /usr/local/etc/roswell # Here the ros binary will be restored: sudo chown "${USER}" /usr/local/bin - if: inputs.cache == 'true' name: Restore Roswell From Cache id: roswell-cache-restore uses: actions/cache/restore@v4 with: path: ${{ inputs.roswell-cache-paths }} key: roswell-${{ inputs.roswell-version }}-${{ steps.locals.outputs.current-month }}-${{ env.cache-name }}-${{ runner.os }}-${{ runner.arch }}-${{ env.LISP }}-${{ inputs.cache-suffix }} - if: inputs.cache == 'true' && steps.roswell-cache-restore.outputs.cache-hit == 'true' name: Restore Path To Cached Files shell: lispsh -eo pipefail {0} run: | echo $HOME/.roswell/bin >> $GITHUB_PATH echo .qlot/bin >> $GITHUB_PATH if [[ "$RUNNER_OS" == "Windows" ]]; then echo $HOME/.roswell/lisp/quicklisp/bin >> $GITHUB_PATH fi # Start the piece which results should be cached # On Windows we install roswell using Pacman package manager and don't need this step - if: (inputs.cache == 'false' || steps.roswell-cache-restore.outputs.cache-hit != 'true') && runner.os != 'Windows' name: Install Roswell shell: lispsh -eo pipefail {0} run: | echo ::group::Installing Roswell dependencies if [[ "$RUNNER_OS" == "Linux" ]]; then sudo apt-get update sudo apt-get -y install git build-essential automake libcurl4-openssl-dev fi if [[ "$RUNNER_OS" == "macOS" ]]; then brew install automake autoconf curl fi echo ::endgroup:: if [[ "${{ inputs.roswell-version }}" != "latest" ]]; then echo ::group::Installing Roswell ${{ inputs.roswell-version }} curl -L https://raw.githubusercontent.com/roswell/roswell/${{ inputs.roswell-version }}/scripts/install-for-ci.sh | bash -xeo pipefail else echo ::group::Installing latest Roswell curl -L https://raw.githubusercontent.com/roswell/roswell/master/scripts/install-for-ci.sh | bash -xeo pipefail fi echo ::endgroup:: - if: inputs.cache == 'false' || steps.roswell-cache-restore.outputs.cache-hit != 'true' name: Upgrade Quicklisp dists shell: lispsh -eo pipefail {0} run: | # The parent workflow might have caching enabled for Roswell and all # the other Lisp files in general, so it's better to tell Quicklisp # to update all its dists. ros -e "(ql:update-all-dists :prompt nil)" - if: inputs.cache == 'false' || steps.roswell-cache-restore.outputs.cache-hit != 'true' name: Install Quicklisp patch for package-inferred systems shell: lispsh -eo pipefail {0} run: | git clone \ --no-tags \ --single-branch \ --depth=1 \ https://github.com/40ants/quicklisp-client-fix \ ~/.quicklisp-client-fix mkdir -p ~/.roswell cat $GITHUB_ACTION_PATH/load-quicklisp-fix.lisp >> ~/.roswell/init.lisp - if: inputs.cache == 'false' || steps.roswell-cache-restore.outputs.cache-hit != 'true' name: Upgrade ASDF to the Latest Version shell: lispsh -eo pipefail {0} run: | if [[ "${{ inputs.asdf-version }}" != "latest" ]]; then echo ::group::Installing ASDF ${{ inputs.asdf-version }} ros install asdf/${{ inputs.asdf-version }} else echo ::group::Installing latest ASDF ros install asdf fi echo ::endgroup:: - if: inputs.cache == 'false' || steps.roswell-cache-restore.outputs.cache-hit != 'true' name: Install Qlot shell: lispsh -eo pipefail {0} run: | if [[ "${{ inputs.qlot-version }}" != "latest" ]]; then echo ::group::Installing Qlot ${{ inputs.qlot-version }} ros install fukamachi/qlot/${{ inputs.qlot-version }} else echo ::group::Installing latest Qlot ros install fukamachi/qlot fi echo .qlot/bin >> $GITHUB_PATH echo ::endgroup:: - if: inputs.cache == 'true' && steps.roswell-cache-restore.outputs.cache-hit != 'true' name: Cache Roswell Setup id: roswell-cache-save uses: actions/cache/save@v4 with: path: ${{ inputs.roswell-cache-paths }} key: ${{ steps.roswell-cache-restore.outputs.cache-primary-key }} # We really need this step go before cache restore, # because it changes qlfile and cache key depends on it. - name: Ensure qlfile exists shell: lispsh -eo pipefail {0} run: | echo ::group::Ensure qlfile exists if [[ -n "${{ inputs.qlfile-template }}" ]]; then echo "${{ inputs.qlfile-template }}" | $GITHUB_ACTION_PATH/templater.ros > qlfile rm -f qlfile.lock echo "Created qlfile:" echo '===============' cat qlfile echo '===============' echo '' elif [[ -e qlfile ]]; then echo 'Here is content of qlfile:' echo '===============' cat qlfile echo '===============' echo '' else echo 'There is no qlfile. Creating an empty one.' touch qlfile fi echo ::endgroup:: - if: inputs.cache == 'true' name: Restore Qlot Environment id: qlot-cache-restore uses: actions/cache/restore@v4 with: path: ${{ inputs.qlot-cache-paths }} key: qlot-${{ steps.locals.outputs.current-month }}-${{ env.cache-name }}-${{ runner.os }}-${{ runner.arch }}-${{ env.QUICKLISP_DIST }}-${{ env.LISP }}-${{ hashFiles('qlfile', 'qlfile.lock', '*.asd') }}-${{ inputs.cache-suffix }} - if: inputs.cache == 'true' && steps.qlot-cache-restore.outputs.cache-hit == 'true' name: Restore Path To .qlot/bin shell: lispsh -eo pipefail {0} run: | echo .qlot/bin >> $GITHUB_PATH - if: inputs.cache == 'false' || steps.qlot-cache-restore.outputs.cache-hit != 'true' name: Create Qlot Environment shell: lispsh -eo pipefail {0} run: | echo ::group::Create Qlot Environment if [[ "${{ inputs.qlot-no-deps }}" != 'false' ]]; then echo 'Running Qlot with --no-deps argument' qlot install --no-deps else echo 'Running Qlot as usual' qlot install fi echo ::endgroup:: env: QLFILE_TEMPLATE: ${{ inputs.qlfile-template }} # This step will install system and # all possible roswell scripts, if the system # has them in the roswell/ subdirectory: - if: inputs.asdf-system && (inputs.cache == 'false' || steps.qlot-cache-restore.outputs.cache-hit != 'true') name: Install ASDF System shell: lispsh -eo pipefail {0} # Here we'll need to set CL_SOURCE_REGISTRY # when will switch to a new qlot: run: | echo ::group::Install ASDF System qlot exec ros install ${{ inputs.asdf-system }} echo ::endgroup:: - if: inputs.cache == 'true' && steps.qlot-cache-restore.outputs.cache-hit != 'true' name: Cache Qlot Environment id: qlot-cache-save uses: actions/cache/save@v4 with: path: ${{ inputs.qlot-cache-paths }} key: ${{ steps.qlot-cache-restore.outputs.cache-primary-key }} # End of the cached piece - name: Check it is possible to run desired lisp implementation shell: lispsh -eo pipefail {0} # Call ${{ github.action_path }}test.ros does not work on windows # because of backslashes. # # Here we are using sed to transform slashes in the path. # Without this trick it is impossible to run test.ros on Windows. # # The other way to do the trick is to change write path to GITHUB_PATH. # In this case, GitHub itself will convert backslashes. Hovewer, this # way the path will be added to the PATH variable of the workflow # which used setup-lisp and I consider this is not desired behaviour. run: | echo ::group::Checking if we installed correct Lisp implementation if [[ "$RUNNER_OS" == "Windows" ]]; then ACTION_PATH="$(echo '${{ github.action_path }}' | sed -e 's|/|\\|')\\" else ACTION_PATH='${{ github.action_path }}/' fi ${ACTION_PATH}test.ros echo ::endgroup::