hammerspoon/scripts/libbuild.sh

660 lines
26 KiB
Bash

#!/bin/bash
# Helper functions for Hammerspoon build.sh
############################## ERROR FUNCTIONS ##############################
function fail() {
echo "ERROR: $*" >/dev/stderr
exit 1
}
############################# TOP LEVEL COMMANDS #############################
function op_clean() {
echo "Cleaning build folder..."
${RM} -rf "${BUILD_HOME}"
echo "Cleaning temporary build folders..."
xcodebuild -workspace Hammerspoon.xcworkspace -scheme "${XCODE_SCHEME}" -configuration "${XCODE_CONFIGURATION}" -destination "platform=macOS" clean | xcbeautify ${XCB_OPTS[@]:-}
}
function op_build() {
op_build_assert
if [ "${UPLOAD_DSYM}" == "1" ]; then
op_sentry_assert
echo "Importing Sentry token from: ${TOKENPATH}/token-sentry-auth"
# shellcheck disable=SC1090
source "${SENTRY_TOKEN_AUTH_FILE}"
fi
echo "Building..."
${RM} -rf "${HAMMERSPOON_BUNDLE_PATH}"
local BUILD_COMMAND="archive"
if [ "${BUILD_FOR_TESTING}" == "1" ]; then
BUILD_COMMAND="build-for-testing"
fi
# Build the app
echo "-> xcodebuild -workspace Hammerspoon.xcworkspace -scheme ${XCODE_SCHEME} -configuration ${XCODE_CONFIGURATION} -destination \"platform=macOS\" -archivePath ${HAMMERSPOON_XCARCHIVE_PATH} archive | tee ${BUILD_HOME}/${XCODE_CONFIGURATION}-build.log"
xcodebuild -workspace Hammerspoon.xcworkspace \
-scheme "${XCODE_SCHEME}" \
-configuration "${XCODE_CONFIGURATION}" \
-destination "platform=macOS" \
-archivePath "${HAMMERSPOON_XCARCHIVE_PATH}" \
"${BUILD_COMMAND}" | tee "${BUILD_HOME}/${XCODE_CONFIGURATION}-build.log" | xcbeautify ${XCB_OPTS[@]:-}
if [ "${BUILD_COMMAND}" == "archive" ]; then
# Export the app bundle from the archive
xcodebuild -exportArchive -archivePath "${HAMMERSPOON_XCARCHIVE_PATH}" \
-exportOptionsPlist Hammerspoon/Build\ Configs/Archive-Export-Options.plist \
-exportPath "${BUILD_HOME}"
fi
# Upload dSYMs to Sentry if so desired
if [ "${UPLOAD_DSYM}" == "1" ]; then
export SENTRY_ORG="${SENTRY_ORG:-hammerspoon}"
export SENTRY_PROJECT="${SENTRY_PROJECT:-hammerspoon}"
export SENTRY_LOG_LEVEL=error
if [ "${DEBUG}" == "1" ]; then
SENTRY_LOG_LEVEL=debug
fi
export SENTRY_AUTH_TOKEN
"${HAMMERSPOON_HOME}/scripts/sentry-cli" upload-dif "${HAMMERSPOON_XCARCHIVE_PATH}/dSYMs/" 2>&1 | tee "${BUILD_HOME}/sentry-upload.log"
fi
}
function op_test() {
op_test_assert
mkdir -p "${BUILD_HOME}/reports"
# We have to allow things to fail, because test runs may fail and we want the output
set +e
set +o pipefail
#xcodebuild -workspace Hammerspoon.xcworkspace -scheme Release test-without-building
xcodebuild -workspace Hammerspoon.xcworkspace \
-scheme "${XCODE_SCHEME}" \
-configuration "${XCODE_CONFIGURATION}" \
-resultBundlePath "${BUILD_HOME}/TestResults" \
test-without-building 2>&1 | tee "${BUILD_HOME}/test.log" | xcbeautify ${XCB_OPTS[@]:-}
# Re-enable error capture
set -e
set -o pipefail
}
function op_validate() {
echo "Validating ${HAMMERSPOON_BUNDLE_PATH}..."
op_validate_assert
# Obtain the relevant build settings
local BUILD_SETTINGS ; BUILD_SETTINGS=$(xcodebuild -workspace Hammerspoon.xcworkspace -scheme Release -configuration Release -showBuildSettings 2>&1 | grep -E " CODE_SIGN_IDENTITY|DEVELOPMENT_TEAM|CODE_SIGN_ENTITLEMENTS")
local SIGN_IDENTITY ; SIGN_IDENTITY=$(echo "${BUILD_SETTINGS}" | grep CODE_SIGN_IDENTITY | sed -e 's/.* = //')
local SIGN_TEAM ; SIGN_TEAM=$(echo "${BUILD_SETTINGS}" | grep DEVELOPMENT_TEAM | sed -e 's/.* = //')
local ENTITLEMENTS_FILE ; ENTITLEMENTS_FILE=$(echo "${BUILD_SETTINGS}" | grep CODE_SIGN_ENTITLEMENTS | sed -e 's/.* = //')
# Validate that the app bundle has a correct signature at all
if ! codesign --verify "${HAMMERSPOON_BUNDLE_PATH}" ; then
codesign -dvv "${HAMMERSPOON_BUNDLE_PATH}"
fail "Invalid signature"
fi
echo " ✅ App bundle is signed"
# Fetch the app bundle's relevant signature data
local APP_SIGNATURE ; APP_SIGNATURE=$(codesign --display --verbose=4 "${HAMMERSPOON_BUNDLE_PATH}" 2>&1 | grep ^Authority | head -1)
# Check that the signing team is correct (this is the bit that looks like ABCDEF123G)
# shellcheck disable=SC2001
if [ "$SIGN_TEAM" != "$(echo "${APP_SIGNATURE}" | sed -e 's/.*(\(.*\))/\1/')" ]; then
fail "App is signed with the wrong key: $APP_SIGNATURE (expecting $SIGN_TEAM)"
fi
echo " ✅ Signing team is correct (${SIGN_TEAM})"
# Check that the signing identity is correct (typically this should be "Developer ID Application")
# shellcheck disable=SC2001
if [ "${SIGN_IDENTITY}" != "$(echo "${APP_SIGNATURE}" | sed -e 's/.*=\(.*\):.*/\1/')" ]; then
fail "App is signed with the wrong identity: $APP_SIGNATURE (expecting $SIGN_IDENTITY)"
fi
echo " ✅ Signing identity is correct (${SIGN_IDENTITY})"
# Check that Gatekeepr accepts the app bundle
if ! spctl --assess --type execute "${HAMMERSPOON_BUNDLE_PATH}" ; then
spctl --verbose=4 --assess --type execute "${HAMMERSPOON_BUNDLE_PATH}"
fail "Gatekeeper rejection:"
fi
echo " ✅ Gatekeeper accepts the app bundle"
# Check that the app bundle has the expected entitlements
local EXPECTED_ENTITLEMENTS ; EXPECTED_ENTITLEMENTS=$(xmllint --c14n --format "${HAMMERSPOON_HOME}/${ENTITLEMENTS_FILE}" 2>/dev/null)
# FIXME: the ':-' syntax is deprecated, when we stop caring about building on <Monterey machines, this is the correct new line
#local ACTUAL_ENTITLEMENTS ; ACTUAL_ENTITLEMENTS=$(codesign --display --entitlements - --xml "${HAMMERSPOON_BUNDLE_PATH}" | xmllint --c14n --format -)
local ACTUAL_ENTITLEMENTS ; ACTUAL_ENTITLEMENTS=$(codesign --display --entitlements :- "${HAMMERSPOON_BUNDLE_PATH}" | xmllint --c14n --format -)
if [ "${EXPECTED_ENTITLEMENTS}" != "${ACTUAL_ENTITLEMENTS}" ]; then
echo "***** EXPECTED ENTITLEMENTS (${ENTITLEMENTS_FILE}):"
echo "${EXPECTED_ENTITLEMENTS}"
echo "***** ACTUAL ENTITLEMENTS:"
echo "${ACTUAL_ENTITLEMENTS}"
echo "*****"
fail "Entitlements did not apply correctly"
fi
echo " ✅ Entitlements are as expected"
echo ""
echo " 🎉 ${HAMMERSPOON_BUNDLE_PATH} is fully valid"
}
function op_docs() {
op_docs_assert
local LSDOCSDIR="${BUILD_HOME}/html/LuaSkin"
local DOCSCRIPT="${HAMMERSPOON_HOME}/scripts/docs/bin/build_docs.py"
pushd "${HAMMERSPOON_HOME}" >/dev/null || fail "Unable to access Hammerspoon repo at ${HAMMERSPOON_HOME}"
if [ "${DOCS_LINT_ONLY}" == 1 ]; then
"${DOCSCRIPT}" -l ${DOCS_SEARCH_DIRS[*]} || fail "Docs lint failed"
echo "Docs lint OK"
popd >/dev/null || fail "Unknown"
return # We return here because this option cannot be used with any of the subsequent ones
fi
if [ "${DOCS_JSON}" == 1 ]; then
echo "Building docs JSON..."
"${DOCSCRIPT}" -o "${BUILD_HOME}" --json ${DOCS_SEARCH_DIRS[@]}
fi
if [ "${DOCS_MD}" == 1 ]; then
echo "Building docs Markdown..."
"${DOCSCRIPT}" -o "${BUILD_HOME}" --markdown ${DOCS_SEARCH_DIRS[@]}
fi
if [ "${DOCS_HTML}" == 1 ]; then
echo "Building docs HTML..."
"${DOCSCRIPT}" -o "${BUILD_HOME}" --html ${DOCS_SEARCH_DIRS[@]}
fi
if [ "${DOCS_SQL}" == 1 ]; then
echo "Building docs SQLite..."
"${DOCSCRIPT}" -o "${BUILD_HOME}" --sql ${DOCS_SEARCH_DIRS[@]}
fi
if [ "${DOCS_DASH}" == 1 ]; then
echo "Building docs Dash..."
local DASHDIR="${BUILD_HOME}/Hammerspoon.docset"
${RM} -rf "${DASHDIR}"
${RM} -rf "${LSDOCSDIR}"
cp -R "${HAMMERSPOON_HOME}/scripts/docs/templates/Hammerspoon.docset" "${DASHDIR}"
cp "${BUILD_HOME}/docs.sqlite" "${DASHDIR}/Contents/Resources/docSet.dsidx"
cp "${HAMMERSPOON_HOME}"/build/html/* "${DASHDIR}/Contents/Resources/Documents/"
tar -cvf "${BUILD_HOME}/Hammerspoon.tgz" -C "${BUILD_HOME}" Hammerspoon.docset >"${BUILD_HOME}/docset-tar.log" 2>&1
fi
if [ "${DOCS_LUASKIN}" == 1 ]; then
echo "Building docs LuaSkin..."
mkdir -p "${LSDOCSDIR}"
headerdoc2html -u -o "${LSDOCSDIR}" "${HAMMERSPOON_HOME}/LuaSkin/LuaSkin/Skin.h" >"${BUILD_HOME}/luaskin-headerdoc.log" 2>&1
resolveLinks "${LSDOCSDIR}" >"${BUILD_HOME}/luaskin-resolveLinks.log" 2>&1
mv "${LSDOCSDIR}"/Skin_h/* "${LSDOCSDIR}"
rmdir "${LSDOCSDIR}/Skin_h"
fi
echo "Docs built"
popd >/dev/null || fail "Unknown"
}
function op_installdeps() {
echo "Installing dependencies..."
echo " Homebrew packages..."
brew install coreutils jq xcbeautify gawk cocoapods gh || fail "Unable to install Homebrew dependencies"
echo " Python packages..."
/usr/bin/pip3 install --user --disable-pip-version-check -r "${HAMMERSPOON_HOME}/requirements.txt" || fail "Unable to install Python dependencies"
if [ "${INSTALLDEPS_FULL}" == "1" ]; then
echo " Ruby packages..."
/usr/bin/gem install --user t || fail "Unable to install Ruby dependencies"
fi
}
function op_keychain_prep() {
echo " Preparing keychain..."
op_keychain_prep_assert
local SECBIN="/usr/bin/security"
local KEYCHAIN="build.keychain"
# Note: This will fail if KEYCHAIN_PASSPHRASE isn't set in the environment.
# This is explicitly undocumented because this really shouldn't be called anywhere other than CI
if [ "${P12_FILE}" != "" ]; then
echo " Creating new default keychain: ${KEYCHAIN}"
"${SECBIN}" create-keychain -p "${KEYCHAIN_PASSPHRASE}" "${KEYCHAIN}"
"${SECBIN}" default-keychain -s "${KEYCHAIN}"
echo " Unlocking keychain..."
"${SECBIN}" unlock-keychain -p "${KEYCHAIN_PASSPHRASE}" "${KEYCHAIN}"
echo " Importing signing certificate/key..."
"${SECBIN}" import "${P12_FILE}" -f pkcs12 -k "${KEYCHAIN}" -P "${KEYCHAIN_PASSPHRASE}" -T /usr/bin/codesign -x
echo " Removing keychain autolocking settings..."
"${SECBIN}" set-keychain-settings -t 1200
echo " Setting permissions for keychain..."
"${SECBIN}" -q set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "${KEYCHAIN_PASSPHRASE}" "${KEYCHAIN}"
echo " Listing keychains:"
"${SECBIN}" list-keychains -d user
echo " Dumping keychain identity:"
"${SECBIN}" find-identity -v
fi
if [ "${NOTARIZATION_CREDS_FILE}" != "" ]; then
source "${NOTARIZATION_CREDS_FILE}"
local SIGN_TEAM ; SIGN_TEAM=$(xcodebuild -workspace Hammerspoon.xcworkspace -scheme Release -configuration Release -showBuildSettings 2>&1 | grep -E " DEVELOPMENT_TEAM" | sed -e 's/.* = //')
xcrun notarytool store-credentials --sync "${KEYCHAIN_PROFILE}" --apple-id "${NOTARIZATION_USERNAME}" --team-id "${SIGN_TEAM}" --password "${NOTARIZATION_PASSWORD}"
unset NOTARIZATION_USERNAME
unset NOTARIZATION_PASSWORD
fi
}
function op_keychain_post() {
echo " Removing keychain..."
local SECBIN="/usr/bin/security"
local KEYCHAIN="build.keychain"
"${SECBIN}" delete-keychain "${KEYCHAIN}"
}
function op_notarize() {
echo " Notarizing ${NOTARIZATION_FILE:-${HAMMERSPOON_BUNDLE_PATH}}..."
op_notarize_assert
local IS_ZIP=0
if [ "${NOTARIZATION_FILE}" == "" ]; then
echo " Zipping..."
local ZIP_PATH="${HAMMERSPOON_BUNDLE_PATH}.zip"
create_zip "${HAMMERSPOON_BUNDLE_PATH}" "${ZIP_PATH}"
NOTARIZATION_FILE="${ZIP_PATH}"
IS_ZIP=1
fi
echo " Uploading to Apple Notary Service (may take many minutes)..."
local UPLOAD_OUTPUT ; UPLOAD_OUTPUT=$(xcrun notarytool submit "${NOTARIZATION_FILE}" --keychain-profile "${KEYCHAIN_PROFILE}" --wait -f json)
local UPLOAD_ID ; UPLOAD_ID=$(echo "${UPLOAD_OUTPUT}" | jq -r .id)
local UPLOAD_STATUS ; UPLOAD_STATUS=$(echo "${UPLOAD_OUTPUT}" | jq -r .status)
local UPLOAD_MSG ; UPLOAD_MSG=$(echo "${UPLOAD_OUTPUT}" | jq -r .message)
echo " Fetching notarization log..."
xcrun notarytool log "${UPLOAD_ID}" --keychain-profile "${KEYCHAIN_PROFILE}" "${BUILD_HOME}/notarization-log.json"
if [ "${UPLOAD_STATUS}" != "Accepted" ]; then
echo "Notarization upload is in an unexpected state: ${UPLOAD_STATUS} (${UPLOAD_MSG})"
echo "Upload log follows:"
cat build/notarization-log.json
fail "Unable to continue"
fi
echo " Stapling notarization ticket..."
xcrun stapler staple "${HAMMERSPOON_BUNDLE_PATH}"
echo " Validating notarization..."
if ! xcrun stapler validate "${HAMMERSPOON_BUNDLE_PATH}" ; then
fail "Notarization rejection"
fi
if [ "${IS_ZIP}" == "1" ]; then
# Remove the zip we uploaded for Notarization
${RM} "${HAMMERSPOON_BUNDLE_PATH}.zip"
# At this stage we don't know if this is a full release build or a CI build, so prepare a notarized zip for both
create_zip "${HAMMERSPOON_BUNDLE_PATH}" "${HAMMERSPOON_BUNDLE_PATH}-$(release_version).zip"
create_zip "${HAMMERSPOON_BUNDLE_PATH}" "${HAMMERSPOON_BUNDLE_PATH}-$(nightly_version).zip"
fi
echo " ✅ Notarization successful!"
}
function op_archive() {
local VERSION ; VERSION="$(get_version)"
local ARCHIVE_PATH="${HAMMERSPOON_HOME}/../archive/${VERSION}"
echo "Archiving to ${ARCHIVE_PATH}..."
mkdir -p "${ARCHIVE_PATH}"
# Archive the final zip, the xcarchive, and all the build/notarization/sentry logfiles
cp -a "${HAMMERSPOON_BUNDLE_PATH}-${VERSION}.zip" "${ARCHIVE_PATH}/"
cp -a "${HAMMERSPOON_XCARCHIVE_PATH}" "${ARCHIVE_PATH}/"
cp -a "${BUILD_HOME}"/*.log "${ARCHIVE_PATH}/"
cp -a "${BUILD_HOME}"/*.plist "${ARCHIVE_PATH}/"
# Dump dSYM UUIDs and archive them
find "${HAMMERSPOON_XCARCHIVE_PATH}" -name '*.dSYM' -exec dwarfdump -u {} \; >"${ARCHIVE_PATH}/dSYM_UUID.txt"
create_zip "${HAMMERSPOON_XCARCHIVE_PATH}/dSYMs" "${ARCHIVE_PATH}/${APP_NAME}-dSYM-${VERSION}.zip"
# Archive the docs
mkdir -p "${ARCHIVE_PATH}/docs"
cp -a "${BUILD_HOME}/docs.json" "${ARCHIVE_PATH}/docs/"
create_zip "${BUILD_HOME}/html" "${ARCHIVE_PATH}/docs/${VERSION}-docs.zip"
}
function op_release() {
local VERSION ; VERSION="$(release_version)"
op_release_assert
# We always do a local test of the signed/notarized build, to ensure it runs
echo "Opening Finder for a local test..."
open -R "${HAMMERSPOON_BUNDLE_PATH}"
echo -n "******** TEST THE BUILD PLEASE ('yes' to confirm it works): "
local REPLY=""
read -r REPLY
if [ "${REPLY}" != "yes" ]; then
fail "User rejected build"
fi
# Prepaer the release archive
echo " Zipping..."
# FIXME: HAMMERSPOON_BUNDLE_PATH here is not right, that gives us Hammerspoon.app-X.Y.Z.zip and we don't want the .app
local ZIP_PATH="${BUILD_HOME}/${APP_NAME}-${VERSION}.zip"
rm -f "${ZIP_PATH}"
create_zip "${HAMMERSPOON_BUNDLE_PATH}" "${ZIP_PATH}"
echo " Creating release on GitHub..."
gh release create "${VERSION}" "${ZIP_PATH}" --title "${VERSION}" --notes-file "${WEBSITE_HOME}/_posts/$(date "+%Y-%m-%d")-${VERSION}.md"
echo " Uploading docs to website..."
pushd "${WEBSITE_HOME}" >/dev/null || fail "Unable to access website repo at ${WEBSITE_HOME}"
mkdir -p "docs/${VERSION}"
${RM} docs/*.html
${RM} -rf docs/LuaSkin
cp -r "${BUILD_HOME}/html/" docs/
cp -r "${BUILD_HOME}/html/" "docs/${VERSION}/"
git add .
git commit --allow-empty -am "Add docs for ${VERSION}"
git push
popd >/dev/null || fail "Unknown"
echo " Creating PR for Dash docs..."
pushd "${HAMMERSPOON_HOME}/../" >/dev/null || fail "Unable to access ${HAMMERSPOON_HOME}/../"
${RM} -rf dash
git clone -q git@github.com:Kapeli/Dash-User-Contributions.git dash
cp "${BUILD_HOME}/Hammerspoon.tgz" dash/docsets/Hammerspoon/
pushd "dash" >/dev/null || fail "Unable to access dash repo at: ${HAMMERSPOON_HOME}/../dash"
git remote add hammerspoon git@github.com:hammerspoon/Dash-User-Contributions.git
git checkout -b "hammerspoon-${VERSION}"
cat >docsets/Hammerspoon/docset.json <<EOF
{
"name": "Hammerspoon",
"version": "${VERSION}",
"archive": "Hammerspoon.tgz",
"author": {
"name": "Hammerspoon Team",
"link": "https://www.hammerspoon.org/"
},
"aliases": [],
"specific_versions": [
]
}
EOF
git add docsets/Hammerspoon/Hammerspoon.tgz
git commit -qam "Update Hammerspoon docset to ${VERSION}"
git push -qfv hammerspoon master
gh pr create --body "" --title "Update Hammerspoon docset to ${VERSION}"
popd >/dev/null || fail "Unknown"
popd >/dev/null || fail "Unknown"
echo " Updating appcast.xml..."
eval $(stat -s "${ZIP_PATH}")
export ZIPLEN="${st_size}"
pushd "${HAMMERSPOON_HOME}/" >/dev/null || fail "Unable to access ${HAMMERSPOON_HOME}/"
local BUILD_NUMBER ; BUILD_NUMBER=$(git rev-list "$(git symbolic-ref HEAD | sed -e 's,.*/\\(.*\\),\\1,')" --count)
local NEWCHUNK ; NEWCHUNK="<!-- __UPDATE_MARKER__ -->
<item>
<title>Version ${VERSION}</title>
<sparkle:releaseNotesLink>
https://www.hammerspoon.org/releasenotes/${VERSION}.html
</sparkle:releaseNotesLink>
<pubDate>$(date +"%a, %e %b %Y %H:%M:%S %z")</pubDate>
<enclosure url=\"https://github.com/Hammerspoon/hammerspoon/releases/download/${VERSION}/Hammerspoon-${VERSION}.zip\"
sparkle:version=\"${BUILD_NUMBER}\"
sparkle:shortVersionString=\"${VERSION}\"
length=\"${ZIPLEN}\"
type=\"application/octet-stream\"
/>
<sparkle:minimumSystemVersion>10.15</sparkle:minimumSystemVersion>
</item>
"
gawk -i inplace -v s="<!-- __UPDATE_MARKER__ -->" -v r="${NEWCHUNK}" '{gsub(s,r)}1' appcast.xml
git add appcast.xml
git commit -qam "Update appcast.xml for ${VERSION}"
git push
popd >/dev/null || fail "Unknown"
echo " Updating Sentry release..."
export SENTRY_ORG="${SENTRY_ORG:-hammerspoon}"
export SENTRY_PROJECT="${SENTRY_PROJECT:-hammerspoon}"
export SENTRY_LOG_LEVEL=error
if [ "${DEBUG}" == "1" ]; then
SENTRY_LOG_LEVEL=debug
fi
export SENTRY_AUTH_TOKEN
"${HAMMERSPOON_HOME}/scripts/sentry-cli" releases set-commits --auto "${VERSION}" 2>&1 | tee "${BUILD_HOME}/sentry-release.log"
"${HAMMERSPOON_HOME}/scripts/sentry-cli" releases finalize "${VERSION}" 2>&1 | tee -a "${BUILD_HOME}/sentry-release.log"
if [ "${TWITTER_ACCOUNT}" != "" ]; then
echo " Tweeting release..."
local T_PATH=$(/usr/bin/gem contents t 2>/dev/null | grep "\/t$")
local CURRENT_T_ACCOUNT ; CURRENT_T_ACCOUNT=$("${T_PATH}" accounts | grep -B1 active | head -1)
"${T_PATH}" set active "${TWITTER_ACCOUNT}"
"${T_PATH}" update "Just released ${VERSION} - https://www.hammerspoon.org/releasenotes/"
"${T_PATH}" set active "${CURRENT_T_ACCOUNT}"
fi
}
############################## COMMAND ASSERTIONS ##############################
function op_build_assert() {
echo "Checking build environment..."
assert_gawk
assert_xcbeautify
assert_cocoapods_state
if [ "${XCODE_CONFIGURATION}" == "Release" ]; then
if [ ! -f "${SENTRY_TOKEN_API_FILE}" ]; then
fail "Release build requested, but no Sentry API token exists at: ${SENTRY_TOKEN_API_FILE}"
fi
fi
}
function op_test_assert() {
# Nothing to assert here for now
return
}
function op_docs_assert() {
echo "Checking docs environment..."
# We only need to assert requirements.txt satisfaction if we're going to be generating output that needs the modules it specifies
if [ "${DOCS_MD}" == 1 ] || [ "${DOCS_HTML}" == 1 ] || [ "${DOCS_DASH}" == 1 ]; then
assert_docs_requirements
fi
}
function op_installdeps_assert() {
echo "Checking environment..."
if [ ! "$(which brew)" ]; then
echo "Unable to continue without Homebrew installed, please see: https://brew.sh/"
exit 1
fi
}
function op_validate_assert() {
if [ ! -e "${HAMMERSPOON_BUNDLE_PATH}" ]; then
fail "Unable to validate ${HAMMERSPOON_BUNDLE_PATH}, it doesn't exist"
fi
}
function op_keychain_prep_assert() {
if [ "${IS_CI}" != "1" ]; then
echo "You almost certainly don't want to do this keychain preparation outside of CI"
echo "If you are absolutely sure that you do want to, export IS_CI=1"
echo " BE WARNED: If you force this to run and give it a P12 file, it will create a new default keychain on your Mac"
fail "Refusing to continue"
fi
if [ "${P12_FILE}" == "" ] && [ "${NOTARIZATION_CREDS_FILE}" == "" ]; then
fail "Can't prepare a keychain without either a P12 file or a Notarization Credentials file (or both)"
fi
if [ "${P12_FILE}" != "" ] && [ ! -e "${P12_FILE}" ]; then
fail "Unable to access P12 signing certificate: ${P12_FILE}"
fi
if [ "${NOTARIZATION_CREDS_FILE}" != "" ] && [ ! -e "${NOTARIZATION_CREDS_FILE}" ]; then
fail "Unable to access Notarization credentials file: ${NOTARIZATION_CREDS_FILE}"
fi
}
function op_notarize_assert() {
# FIXME: Figure out a way to assert that the keychain profile exists
return
}
function op_archive_assert() {
if [ ! -e "${HAMMERSPOON_BUNDLE_PATH}-${VERSION}.zip" ]; then
fail "Unable to archive: ${HAMMERSPOON_BUNDLE_PATH}-${VERSION}.zip is missing"
fi
if [ ! -e "${HAMMERSPOON_XCARCHIVE_PATH}" ]; then
fail "Unable to archive: ${HAMMERSPOON_XCARCHIVE_PATH} is missing"
fi
if [ ! -e "${BUILD_HOME}/docs.json" ]; then
fail "Unable to archive: ${BUILD_HOME}/docs.json is missing"
fi
if [ ! -e "${BUILD_HOME}/html" ]; then
fail "Unable to archive: ${BUILD_HOME}/html is missing"
fi
}
function op_sentry_assert() {
if [ ! -f "${SENTRY_TOKEN_AUTH_FILE}" ]; then
fail "You do not have a Sentry auth tokens in ${SENTRY_TOKEN_AUTH_FILE}"
fi
}
function op_release_assert() {
echo "Checking release notes exist..."
local RNOTES ; RNOTES="${WEBSITE_HOME}/_posts/$(date "+%Y-%m-%d")-${VERSION}.md"
if [ ! -f "${RNOTES}" ]; then
fail "Unable to find expected release notes: ${RNOTES}"
fi
echo "Checking GitHub login status..."
if ! gh auth status >/dev/null 2>&1 ; then
echo " gh not logged in, trying with ${GITHUB_TOKEN_FILE}"
# GitHub CLI client is not currently logged in, let's see if we have a token available and can fix it
if [ ! -f "${GITHUB_TOKEN_FILE}" ]; then
fail "You do not have a GitHub auth token in ${GITHUB_TOKEN_FILE}. Generate one with 'read:org, repo' permissions at: https://github.com/settings/tokens, or run 'gh auth login'"
fi
gh auth login --with-token <"${GITHUB_TOKEN_FILE}"
if ! gh auth status >/dev/null 2>&1 ; then
fail "Unable to login to GitHub with token in ${GITHUB_TOKEN_FILE}"
fi
fi
# Ensure we have a full tag for the release
echo "Checking release tag..."
pushd "${HAMMERSPOON_HOME}" >/dev/null || fail "Unable to access ${HAMMERSPOON_HOME}"
local TAGTYPE ; TAGTYPE="$(git cat-file -t "${VERSION}")"
if [ "${TAGTYPE}" != "tag" ]; then
fail "${VERSION} is not an annotated tag, it is either missing or is a lightweight tag. Use: git tag -a ${VERSION}"
fi
popd >/dev/null || fail "Unknown"
# Ensure this tag is not already released
if gh release view "${VERSION}" >/dev/null 2>&1 ; then
fail "${VERSION} already exists on GitHub, cannot re-release"
fi
# Check that the website repo is present, has no uncommitted changes, and is in-sync with upstream
pushd "${WEBSITE_HOME}" >/dev/null || fail "Website repo missing/inaccessible at: ${WEBSITE_HOME}"
if ! git diff-index --quiet HEAD -- ; then
fail "Website repo has uncommitted changes, please commit or stash them before releasing"
fi
git fetch origin
local DESYNC
DESYNC="$(git rev-list --left-right "@{upstream}"...HEAD)"
if [ "${DESYNC}" != "" ]; then
fail "Website repo is out of sync with GitHub, please sync before releasing"
fi
popd >/dev/null || fail "Unknown"
}
############################## ASSERTION HELPERS ###############################
function assert_gawk() {
if [ "$(which gawk)" == "" ]; then
fail "gawk doesn't seem to be in your PATH. Try $0 installdeps"
fi
}
function assert_xcbeautify() {
if [ "$(which xcbeautify)" == "" ]; then
fail "xcbeautify is not in PATH. Try $0 installdeps"
fi
}
function assert_docs_requirements() {
# FIXME: This is overly broad - if all that's happening is linting or JSON generation, these requirements are not required
echo "Checking Python requirements.txt is satisfied..."
echo "import sys
import pkg_resources
from pkg_resources import DistributionNotFound, VersionConflict
dependencies = open('${HAMMERSPOON_HOME}/requirements.txt', 'r').readlines()
pkg_resources.require(dependencies)" | /usr/bin/python3
}
function assert_cocoapods_state() {
echo "Checking Cocoapods state..."
pushd "${HAMMERSPOON_HOME}" >/dev/null || fail "Unable to enter ${HAMMERSPOON_HOME}"
if ! pod outdated >/dev/null 2>&1 ; then
fail "cocoapods installation does not seem sane"
fi
popd >/dev/null || fail "Unknown"
}
############################## UTILITY HELPERS ###############################
function get_version() {
if [ "${IS_NIGHTLY}" == "1" ]; then
nightly_version
else
release_version
fi
}
function release_version() {
local VERSION ; VERSION=$(cd "${HAMMERSPOON_HOME}" || fail "Unable to enter ${HAMMERSPOON_HOME}" ; git describe --abbrev=0)
echo "${VERSION}"
}
function nightly_version() {
local VERSION ; VERSION=$(cd "${HAMMERSPOON_HOME}" || fail "Unable to enter ${HAMMERSPOON_HOME}" ; git describe)
echo "${VERSION}"
}
function create_zip() {
local SRC ; SRC="${1}"
local DST ; DST="${2}"
/usr/bin/ditto -c -k --keepParent "${SRC}" "${DST}"
}