Skip to content

Commit 3da3b3a

Browse files
authored
Format shell scripts using shfmt (#1638)
Shell scripts are now formatted using shfmt: https://github.com/mvdan/sh/blob/master/cmd/shfmt/shfmt.1.scd This is enforced in CI, and formatting can be applied locally using a new `make format` target. Notably, this also reformats the scripts to use tabs instead of spaces, since tabs are sadly the only way to be able to properly indent bash here documents (which is something the scripts here will be using a lot of soon) without having to resort to mixed tabs and spaces indentation in the same file. (Plus tabs is also the shfmt default style.) GUS-W-16808198.
1 parent b9f7f3f commit 3da3b3a

26 files changed

+893
-845
lines changed

.editorconfig

+35
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
# https://editorconfig.org
2+
root = true
3+
4+
[*]
5+
charset = utf-8
6+
end_of_line = lf
7+
indent_style = space
8+
insert_final_newline = true
9+
trim_trailing_whitespace = true
10+
11+
[*.sh]
12+
binary_next_line = true
13+
# We sadly have to use tabs in shell scripts otherwise we can't indent here documents:
14+
# https://www.gnu.org/software/bash/manual/html_node/Redirections.html#Here-Documents
15+
indent_style = tab
16+
shell_variant = bash
17+
switch_case_indent = true
18+
19+
# Catches scripts that we can't give a .sh file extension, such as the Buildpack API scripts.
20+
[**/bin/**]
21+
binary_next_line = true
22+
indent_style = tab
23+
shell_variant = bash
24+
switch_case_indent = true
25+
26+
[.hatchet/repos/**]
27+
ignore = true
28+
29+
# The setup-ruby GitHub Action creates this directory when caching is enabled, and if
30+
# its not ignored will cause false positives when running shfmt in the CI lint job.
31+
[vendor/bundle/**]
32+
ignore = true
33+
34+
[Makefile]
35+
indent_style = tab

.github/workflows/ci.yml

+8
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@ on:
1010
permissions:
1111
contents: read
1212

13+
env:
14+
# Used by shfmt and more.
15+
FORCE_COLOR: 1
16+
1317
jobs:
1418
lint:
1519
runs-on: ubuntu-24.04
@@ -23,6 +27,10 @@ jobs:
2327
ruby-version: "3.3"
2428
- name: Run ShellCheck
2529
run: make lint-scripts
30+
- name: Run shfmt
31+
uses: docker://mvdan/shfmt:latest
32+
with:
33+
args: "--diff ."
2634
- name: Run Rubocop
2735
run: bundle exec rubocop
2836

Makefile

+8-2
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,26 @@
11
# These targets are not files
2-
.PHONY: lint lint-scripts lint-ruby run publish
2+
.PHONY: lint lint-scripts lint-ruby check-format format run publish
33

44
STACK ?= heroku-24
55
FIXTURE ?= spec/fixtures/python_version_unspecified
66

77
# Converts a stack name of `heroku-NN` to its build Docker image tag of `heroku/heroku:NN-build`.
88
STACK_IMAGE_TAG := heroku/$(subst -,:,$(STACK))-build
99

10-
lint: lint-scripts lint-ruby
10+
lint: lint-scripts check-format lint-ruby
1111

1212
lint-scripts:
1313
@git ls-files -z --cached --others --exclude-standard 'bin/*' '*/bin/*' '*.sh' | xargs -0 shellcheck --check-sourced --color=always
1414

1515
lint-ruby:
1616
@bundle exec rubocop
1717

18+
check-format:
19+
@shfmt --diff .
20+
21+
format:
22+
@shfmt --write --list .
23+
1824
run:
1925
@echo "Running buildpack using: STACK=$(STACK) FIXTURE=$(FIXTURE)"
2026
@docker run --rm -it -v $(PWD):/src:ro --tmpfs /app -e "HOME=/app" -e "STACK=$(STACK)" "$(STACK_IMAGE_TAG)" \

bin/compile

+63-65
Original file line numberDiff line numberDiff line change
@@ -103,16 +103,16 @@ mkdir -p "$CACHE_DIR/.heroku"
103103
mkdir -p .heroku
104104

105105
# The Python installation.
106-
cp -R "$CACHE_DIR/.heroku/python" .heroku/ &> /dev/null || true
106+
cp -R "$CACHE_DIR/.heroku/python" .heroku/ &>/dev/null || true
107107
# A plain text file which contains the current stack being used (used for cache busting).
108-
cp -R "$CACHE_DIR/.heroku/python-stack" .heroku/ &> /dev/null || true
108+
cp -R "$CACHE_DIR/.heroku/python-stack" .heroku/ &>/dev/null || true
109109
# A plain text file which contains the current python version being used (used for cache busting).
110-
cp -R "$CACHE_DIR/.heroku/python-version" .heroku/ &> /dev/null || true
110+
cp -R "$CACHE_DIR/.heroku/python-version" .heroku/ &>/dev/null || true
111111
# A plain text file which contains the current sqlite3 version being used (used for cache busting).
112-
cp -R "$CACHE_DIR/.heroku/python-sqlite3-version" .heroku/ &> /dev/null || true
112+
cp -R "$CACHE_DIR/.heroku/python-sqlite3-version" .heroku/ &>/dev/null || true
113113
# "editable" installations of code repositories, via pip or pipenv.
114114
if [[ -d "$CACHE_DIR/.heroku/src" ]]; then
115-
cp -R "$CACHE_DIR/.heroku/src" .heroku/ &> /dev/null || true
115+
cp -R "$CACHE_DIR/.heroku/src" .heroku/ &>/dev/null || true
116116
fi
117117

118118
# The pre_compile hook. Customers rely on this. Don't remove it.
@@ -124,47 +124,47 @@ source "${BUILDPACK_DIR}/bin/steps/hooks/pre_compile"
124124
# Sticky runtimes. If there was a previous build, and it used a given version of Python,
125125
# continue to use that version of Python in perpetuity.
126126
if [[ -f "$CACHE_DIR/.heroku/python-version" ]]; then
127-
CACHED_PYTHON_VERSION=$(cat "$CACHE_DIR/.heroku/python-version")
127+
CACHED_PYTHON_VERSION=$(cat "$CACHE_DIR/.heroku/python-version")
128128
fi
129129

130130
# We didn't always record the stack version. This code is in place because of that.
131131
if [[ -f "$CACHE_DIR/.heroku/python-stack" ]]; then
132-
CACHED_PYTHON_STACK=$(cat "$CACHE_DIR/.heroku/python-stack")
132+
CACHED_PYTHON_STACK=$(cat "$CACHE_DIR/.heroku/python-stack")
133133
else
134-
CACHED_PYTHON_STACK=$STACK
134+
CACHED_PYTHON_STACK=$STACK
135135
fi
136136

137137
# TODO: Move this into a new package manager handling implementation when adding Poetry support.
138138
# We intentionally don't mention `setup.py` here since it's being removed soon.
139139
if [[ ! -f requirements.txt && ! -f Pipfile && ! -f setup.py ]]; then
140-
puts-warn
141-
puts-warn "Error: Couldn't find any supported Python package manager files."
142-
puts-warn
143-
puts-warn "A Python app on Heroku must have either a 'requirements.txt' or"
144-
puts-warn "'Pipfile' package manager file in the root directory of its"
145-
puts-warn "source code."
146-
puts-warn
147-
puts-warn "Currently the root directory of your app contains:"
148-
puts-warn
149-
# TODO: Overhaul logging helpers so they can handle prefixing multi-line strings, and switch to them.
150-
# shellcheck disable=SC2012 # Using `ls` instead of `find` is absolutely fine for this use case.
151-
ls -1 --indicator-style=slash "${BUILD_DIR}" | sed 's/^/ ! /'
152-
puts-warn
153-
puts-warn "If your app already has a package manager file, check that it:"
154-
puts-warn
155-
puts-warn "1. Is in the top level directory (not a subdirectory)."
156-
puts-warn "2. Has the correct spelling (the filenames are case-sensitive)."
157-
puts-warn "3. Isn't listed in '.gitignore' or '.slugignore'."
158-
puts-warn
159-
puts-warn "Otherwise, add a package manager file to your app. If your app has"
160-
puts-warn "no dependencies, then create an empty 'requirements.txt' file."
161-
puts-warn
162-
puts-warn "For help with using Python on Heroku, see:"
163-
puts-warn "https://devcenter.heroku.com/articles/getting-started-with-python"
164-
puts-warn "https://devcenter.heroku.com/articles/python-support"
165-
puts-warn
166-
meta_set "failure_reason" "package-manager-not-found"
167-
exit 1
140+
puts-warn
141+
puts-warn "Error: Couldn't find any supported Python package manager files."
142+
puts-warn
143+
puts-warn "A Python app on Heroku must have either a 'requirements.txt' or"
144+
puts-warn "'Pipfile' package manager file in the root directory of its"
145+
puts-warn "source code."
146+
puts-warn
147+
puts-warn "Currently the root directory of your app contains:"
148+
puts-warn
149+
# TODO: Overhaul logging helpers so they can handle prefixing multi-line strings, and switch to them.
150+
# shellcheck disable=SC2012 # Using `ls` instead of `find` is absolutely fine for this use case.
151+
ls -1 --indicator-style=slash "${BUILD_DIR}" | sed 's/^/ ! /'
152+
puts-warn
153+
puts-warn "If your app already has a package manager file, check that it:"
154+
puts-warn
155+
puts-warn "1. Is in the top level directory (not a subdirectory)."
156+
puts-warn "2. Has the correct spelling (the filenames are case-sensitive)."
157+
puts-warn "3. Isn't listed in '.gitignore' or '.slugignore'."
158+
puts-warn
159+
puts-warn "Otherwise, add a package manager file to your app. If your app has"
160+
puts-warn "no dependencies, then create an empty 'requirements.txt' file."
161+
puts-warn
162+
puts-warn "For help with using Python on Heroku, see:"
163+
puts-warn "https://devcenter.heroku.com/articles/getting-started-with-python"
164+
puts-warn "https://devcenter.heroku.com/articles/python-support"
165+
puts-warn
166+
meta_set "failure_reason" "package-manager-not-found"
167+
exit 1
168168
fi
169169

170170
# Pipenv Python version support.
@@ -173,21 +173,21 @@ fi
173173
source "${BUILDPACK_DIR}/bin/steps/pipenv-python-version"
174174

175175
if [[ -f runtime.txt ]]; then
176-
# PYTHON_VERSION_SOURCE may have already been set by the pipenv-python-version step.
177-
# TODO: Refactor this and stop pipenv-python-version using runtime.txt as an API.
178-
PYTHON_VERSION_SOURCE=${PYTHON_VERSION_SOURCE:-"runtime.txt"}
179-
puts-step "Using Python version specified in ${PYTHON_VERSION_SOURCE}"
180-
meta_set "python_version_reason" "specified"
176+
# PYTHON_VERSION_SOURCE may have already been set by the pipenv-python-version step.
177+
# TODO: Refactor this and stop pipenv-python-version using runtime.txt as an API.
178+
PYTHON_VERSION_SOURCE=${PYTHON_VERSION_SOURCE:-"runtime.txt"}
179+
puts-step "Using Python version specified in ${PYTHON_VERSION_SOURCE}"
180+
meta_set "python_version_reason" "specified"
181181
elif [[ -n "${CACHED_PYTHON_VERSION:-}" ]]; then
182-
puts-step "No Python version was specified. Using the same version as the last build: ${CACHED_PYTHON_VERSION}"
183-
echo " To use a different version, see: https://devcenter.heroku.com/articles/python-runtimes"
184-
meta_set "python_version_reason" "cached"
185-
echo "${CACHED_PYTHON_VERSION}" > runtime.txt
182+
puts-step "No Python version was specified. Using the same version as the last build: ${CACHED_PYTHON_VERSION}"
183+
echo " To use a different version, see: https://devcenter.heroku.com/articles/python-runtimes"
184+
meta_set "python_version_reason" "cached"
185+
echo "${CACHED_PYTHON_VERSION}" >runtime.txt
186186
else
187-
puts-step "No Python version was specified. Using the buildpack default: ${DEFAULT_PYTHON_VERSION}"
188-
echo " To use a different version, see: https://devcenter.heroku.com/articles/python-runtimes"
189-
meta_set "python_version_reason" "default"
190-
echo "${DEFAULT_PYTHON_VERSION}" > runtime.txt
187+
puts-step "No Python version was specified. Using the buildpack default: ${DEFAULT_PYTHON_VERSION}"
188+
echo " To use a different version, see: https://devcenter.heroku.com/articles/python-runtimes"
189+
meta_set "python_version_reason" "default"
190+
echo "${DEFAULT_PYTHON_VERSION}" >runtime.txt
191191
fi
192192

193193
# Create the directory for .profile.d, if it doesn't exist.
@@ -201,11 +201,11 @@ mkdir -p /app/.heroku/src
201201
# This is (hopefully obviously) because apps end up running from `/app` in production.
202202
# Realpath is used to support use-cases where one of the locations is a symlink to the other.
203203
if [[ "$(realpath "${BUILD_DIR}")" != "$(realpath /app)" ]]; then
204-
# python expects to reside in /app, so set up symlinks
205-
# we will not remove these later so subsequent buildpacks can still invoke it
206-
ln -nsf "$BUILD_DIR/.heroku/python" /app/.heroku/python
207-
ln -nsf "$BUILD_DIR/.heroku/vendor" /app/.heroku/vendor
208-
# Note: .heroku/src is copied in later.
204+
# python expects to reside in /app, so set up symlinks
205+
# we will not remove these later so subsequent buildpacks can still invoke it
206+
ln -nsf "$BUILD_DIR/.heroku/python" /app/.heroku/python
207+
ln -nsf "$BUILD_DIR/.heroku/vendor" /app/.heroku/vendor
208+
# Note: .heroku/src is copied in later.
209209
fi
210210

211211
# Download / Install Python, from pre-build binaries available on Amazon S3.
@@ -221,10 +221,10 @@ source "${BUILDPACK_DIR}/bin/steps/pipenv"
221221
# This allows for people to ship a setup.py application to Heroku
222222

223223
if [[ ! -f requirements.txt ]] && [[ ! -f Pipfile ]]; then
224-
meta_set "setup_py_only" "true"
225-
echo "-e ." > requirements.txt
224+
meta_set "setup_py_only" "true"
225+
echo "-e ." >requirements.txt
226226
else
227-
meta_set "setup_py_only" "false"
227+
meta_set "setup_py_only" "false"
228228
fi
229229

230230
# SQLite3 support.
@@ -251,11 +251,10 @@ meta_time "nltk_downloader_duration" "${nltk_downloader_start_time}"
251251
# In CI, $BUILD_DIR is /app.
252252
# Realpath is used to support use-cases where one of the locations is a symlink to the other.
253253
if [[ "$(realpath "${BUILD_DIR}")" != "$(realpath /app)" ]]; then
254-
rm -rf "$BUILD_DIR/.heroku/src"
255-
deep-cp /app/.heroku/src "$BUILD_DIR/.heroku/src"
254+
rm -rf "$BUILD_DIR/.heroku/src"
255+
deep-cp /app/.heroku/src "$BUILD_DIR/.heroku/src"
256256
fi
257257

258-
259258
# Django collectstatic support.
260259
# The buildpack automatically runs collectstatic for Django applications.
261260
# This is the cause for the majority of build failures on the Python platform.
@@ -265,7 +264,6 @@ collectstatic_start_time=$(nowms)
265264
sub_env "${BUILDPACK_DIR}/bin/steps/collectstatic"
266265
meta_time "django_collectstatic_duration" "${collectstatic_start_time}"
267266

268-
269267
# Programmatically create .profile.d script for application runtime environment variables.
270268

271269
# Set the PATH to include Python / pip / pipenv / etc.
@@ -286,7 +284,7 @@ set_default_env PYTHONPATH "\$HOME"
286284

287285
# Python expects to be in /app, if at runtime, it is not, set
288286
# up symlinks… this can occur when the subdir buildpack is used.
289-
cat <<EOT >> "$PROFILE_PATH"
287+
cat <<EOT >>"$PROFILE_PATH"
290288
if [[ \$HOME != "/app" ]]; then
291289
mkdir -p /app/.heroku
292290
ln -nsf "\$HOME/.heroku/python" /app/.heroku/python
@@ -298,7 +296,7 @@ EOT
298296
# (such as `/tmp/build_<hash>`) back to `/app`. This is not done during the build itself, since later
299297
# buildpacks still need the build time paths.
300298
if [[ "${BUILD_DIR}" != "/app" ]]; then
301-
cat <<EOT >> "$PROFILE_PATH"
299+
cat <<EOT >>"$PROFILE_PATH"
302300
find .heroku/python/lib/python*/site-packages/ -type f -and \( -name '*.egg-link' -or -name '*.pth' -or -name '__editable___*_finder.py' \) -exec sed -i -e 's#${BUILD_DIR}#/app#' {} \+
303301
EOT
304302
fi
@@ -320,9 +318,9 @@ rm -rf "$CACHE_DIR/.heroku/src"
320318
mkdir -p "$CACHE_DIR/.heroku"
321319
cp -R .heroku/python "$CACHE_DIR/.heroku/"
322320
cp -R .heroku/python-version "$CACHE_DIR/.heroku/"
323-
cp -R .heroku/python-stack "$CACHE_DIR/.heroku/" &> /dev/null || true
321+
cp -R .heroku/python-stack "$CACHE_DIR/.heroku/" &>/dev/null || true
324322
if [[ -d .heroku/src ]]; then
325-
cp -R .heroku/src "$CACHE_DIR/.heroku/" &> /dev/null || true
323+
cp -R .heroku/src "$CACHE_DIR/.heroku/" &>/dev/null || true
326324
fi
327325

328326
meta_time "total_duration" "${compile_start_time}"

bin/detect

+19-19
Original file line numberDiff line numberDiff line change
@@ -13,27 +13,27 @@ BUILD_DIR="${1}"
1313
# so that Python projects that are missing some of the required files still pass detection,
1414
# allowing us to show a helpful error message during the build phase.
1515
KNOWN_PYTHON_PROJECT_FILES=(
16-
.python-version
17-
app.py
18-
main.py
19-
manage.py
20-
pdm.lock
21-
Pipfile
22-
Pipfile.lock
23-
poetry.lock
24-
pyproject.toml
25-
requirements.txt
26-
runtime.txt
27-
setup.cfg
28-
setup.py
29-
uv.lock
16+
.python-version
17+
app.py
18+
main.py
19+
manage.py
20+
pdm.lock
21+
Pipfile
22+
Pipfile.lock
23+
poetry.lock
24+
pyproject.toml
25+
requirements.txt
26+
runtime.txt
27+
setup.cfg
28+
setup.py
29+
uv.lock
3030
)
3131

3232
for filename in "${KNOWN_PYTHON_PROJECT_FILES[@]}"; do
33-
if [[ -f "${BUILD_DIR}/${filename}" ]]; then
34-
echo "Python"
35-
exit 0
36-
fi
33+
if [[ -f "${BUILD_DIR}/${filename}" ]]; then
34+
echo "Python"
35+
exit 0
36+
fi
3737
done
3838

3939
# Cytokine incorrectly indents the first line, so we have to leave it empty.
@@ -43,7 +43,7 @@ echo 1>&2
4343
# since during compile the build will still require a package manager file, so it
4444
# makes sense to describe the stricter requirements up front.
4545
# TODO: Overhaul logging helpers so they can handle prefixing multi-line strings, and switch to them.
46-
sed 's/^/ ! /' 1>&2 << EOF
46+
sed 's/^/ ! /' 1>&2 <<EOF
4747
Error: Your app is configured to use the Python buildpack,
4848
but we couldn't find any supported Python project files.
4949

bin/release

+5-5
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,9 @@ set -u
2222
source "${BUILDPACK_DIR}/bin/utils"
2323

2424
if [[ -f "${BUILD_DIR}/manage.py" ]] && is_module_available 'django' && is_module_available 'psycopg2'; then
25-
cat <<EOF
26-
---
27-
addons:
28-
- heroku-postgresql
29-
EOF
25+
cat <<-'EOF'
26+
---
27+
addons:
28+
- heroku-postgresql
29+
EOF
3030
fi

0 commit comments

Comments
 (0)