Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,16 @@ available and can be used to install ports, create packages, and any
other task it supports. The ports installation is owned by the runner,
thus no root access is required to install ports.

The ports tree is synchronised as part of the action, so there is no
need to run `port sync` or `port selfupdate` afterwards. Because the
default ports tree source is served by a rotating pool of mirrors
where an individual mirror is occasionally unreachable or stalled, the
synchronisation is retried a few times with an increasing delay, and
`rsync_options` in `macports.conf` is configured with a transfer
timeout so that a stalled mirror fails promptly instead of hanging.
The last attempt runs with debug output so that a persistent failure
leaves a usable diagnostic in the workflow log.

An [example workflow](#example-workflow) and an [example parameter file](#example-parameters)
are available below. See the GitHub Help Documentation for
[Creating a workflow file](https://help.github.com/en/articles/configuring-a-workflow#creating-a-workflow-file)
Expand Down
8 changes: 7 additions & 1 deletion install_macports
Original file line number Diff line number Diff line change
Expand Up @@ -226,7 +226,13 @@ main()
else
install_from_source
fi


# The installation writes macports.conf, so the rsync timeouts can
# only be set once it is in place — and they have to be in place
# before the ports tree is synchronised.
write_rsync_options
sync_ports

install_ports "${macports_prefix}/etc/setup-macports.yaml"
}

Expand Down
81 changes: 81 additions & 0 deletions subr/macports.sh
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@
: ${macports_group:=$(id -g -n)}
: ${macports_version:='2.12.5'}
: ${macports_prefix:='/opt/local'}
: ${macports_sync_attempts:='4'}
: ${macports_sync_delay:='10'}
: ${macports_rsync_options:='-rtzvl --delete-after --timeout=60'}

macports_install()
{
Expand Down Expand Up @@ -125,6 +128,84 @@ write_sources()
sources_document "$1" > "${macports_prefix}/etc/macports/sources.conf"
}

# The default ports tree source is served by a rotating pool of mirrors
# and an unresponsive mirror otherwise makes rsync hang for as long as
# the connection is held open. The timeout below turns such a stall
# into a prompt failure, which is what makes retrying worthwhile.
#
# Only --timeout is used: macOS 14 ships rsync 2.6.9, which rejects the
# --contimeout added in rsync 3.1.0, while recent versions ship
# openrsync, which accepts it. A connection that is never established
# fails on its own through the TCP timeout anyway.
write_rsync_options()
{
local pathname stagedfile

pathname="${macports_prefix}/etc/macports/macports.conf"
stagedfile="${pathname}.setup-macports"

if [ -f "${pathname}" ]; then
grep -v '^[[:space:]]*#*[[:space:]]*rsync_options[[:space:]]'\
"${pathname}" > "${stagedfile}" || :
else
: > "${stagedfile}"
fi
printf 'rsync_options %s\n' "${macports_rsync_options}" >> "${stagedfile}"
mv -f "${stagedfile}" "${pathname}"
}

sync_ports_diagnostic()
{
printf 'MacPorts version:\n'
port version || :
printf 'Ports tree sources:\n'
ls -la "${macports_prefix}/var/macports/sources" || :
}

# Synchronising the ports tree reaches out to a mirror pool where an
# individual mirror is sometimes unreachable or stalled. Such failures
# are transient, so a handful of attempts is usually enough to get a
# healthy mirror.
sync_ports()
{
local attempt delay syncflag

attempt='1'
while :; do
if [ "${attempt}" -ge "${macports_sync_attempts}" ]; then
# Ask for debug output on the last attempt, so that a
# persistent failure leaves something to triage without
# paying for an extra synchronisation.
syncflag='-d'
else
syncflag=''
fi

if sudo port ${syncflag} sync; then
return 0
fi

wlog 'Warning'\
'Synchronisation of the ports tree failed on attempt %s of %s.'\
"${attempt}" "${macports_sync_attempts}"

if [ "${attempt}" -ge "${macports_sync_attempts}" ]; then
break
fi

delay=$(expr "${attempt}" \* "${macports_sync_delay}")
wlog 'Info' 'Retrying the synchronisation in %s seconds.' "${delay}"
sleep "${delay}"
attempt=$(expr "${attempt}" + 1)
done

with_group_presentation\
'Ports Tree Synchronisation Diagnostic'\
sync_ports_diagnostic
failwith 'Cannot synchronise the ports tree after %s attempts.'\
"${macports_sync_attempts}"
}

make_package()
{
local macos version
Expand Down
194 changes: 194 additions & 0 deletions testsuite/modules/macports
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
#!/bin/sh

# macports — Testsuite for MacPorts functions

# Paranext Actions (https://github.com/paranext/setup-macports)
# This file is part of Paranext Actions.
#
# Copyright © 2022–2023 Michaël Le Barbier (original author)
# Copyright © 2025 SIL Global and United Bible Societies (subsequent changes)
# All rights reserved.

# This file must be used under the terms of the MIT License.
# This source file is licensed as described in the file LICENSE, which
# you should have received as part of this distribution. The terms
# are also available at https://opensource.org/licenses/MIT

: ${TOPLEVELDIR:=$(git rev-parse --show-toplevel)}
: ${subrdir:=${TOPLEVELDIR}/subr}

. "${subrdir}/stdlib.sh"
. "${subrdir}/macports.sh"
. "${subrdir}/testsuite.sh"

# The tests below run in a dedicated working directory, so the ports
# tree synchronisation can be exercised without touching a real
# MacPorts installation and without waiting for the network.

assume_that_port_sync_always_fails()
{
sudo()
(
printf 'attempt\n' >> 'attempts'
exit 1
)

sleep()
(
printf '%s\n' "$1" >> 'delays'
exit 0
)
}

assume_that_port_sync_succeeds_on_attempt()
{
successful_attempt="$1"

sudo()
(
printf 'attempt\n' >> 'attempts'
if [ "$(wc -l < 'attempts')" -lt "${successful_attempt}" ]; then
exit 1
fi
exit 0
)

sleep()
(
printf '%s\n' "$1" >> 'delays'
exit 0
)
}

count_attempts()
(
if [ -f 'attempts' ]; then
wc -l < 'attempts' | tr -d ' '
else
printf '0\n'
fi
)

assert_that_a_successful_synchronisation_is_not_retried()
(
assume_that_port_sync_succeeds_on_attempt 1
sync_ports
exitcode="$?"
set -e
test "${exitcode}" -eq 0
test "$(count_attempts)" -eq 1
)

assert_that_a_failed_synchronisation_is_retried()
(
assume_that_port_sync_succeeds_on_attempt 3
sync_ports
exitcode="$?"
set -e
test "${exitcode}" -eq 0
test "$(count_attempts)" -eq 3
)

assert_that_synchronisation_gives_up_after_the_last_attempt()
(
macports_sync_attempts='4'
assume_that_port_sync_always_fails
( sync_ports )
exitcode="$?"
set -e
test "${exitcode}" -ne 0
test "$(count_attempts)" -eq 4
)

assert_that_synchronisation_waits_between_attempts()
(
macports_sync_attempts='3'
assume_that_port_sync_always_fails
( sync_ports )
set -e
# One delay fewer than attempts: we do not wait after giving up.
test "$(wc -l < 'delays' | tr -d ' ')" -eq 2
# Delays increase, so a mirror under load is given more time.
test "$(sed -n 1p 'delays')" -lt "$(sed -n 2p 'delays')"
)

assert_that_rsync_options_are_written_when_absent()
(
macports_prefix="$(pwd)"
mkdir -p 'etc/macports'
write_rsync_options
set -e
grep -q '^rsync_options .*--timeout=' 'etc/macports/macports.conf'
)

# We replace the rsync_options setting wholesale, so the flags MacPorts
# itself defaults to have to be carried over: dropping -l in
# particular would stop symlinks in the ports tree being copied.
assert_that_rsync_options_keep_the_macports_defaults()
(
macports_prefix="$(pwd)"
mkdir -p 'etc/macports'
write_rsync_options
set -e
grep -q '^rsync_options -rtzvl --delete-after' 'etc/macports/macports.conf'
)

# Configured rsync options are only useful if the rsync that MacPorts
# actually runs accepts them, and support differs across the runners we
# support: macOS 14 ships rsync 2.6.9, which has no --contimeout, while
# recent versions ship openrsync, which does. Anything we configure has
# to be accepted by all of them.
assert_that_rsync_accepts_the_configured_options()
(
rsync_program='/usr/bin/rsync'
if [ ! -x "${rsync_program}" ]; then
rsync_program=$(command -v rsync) || exit 0
fi

mkdir -p 'src' 'dst'
: > 'src/a'
output=$("${rsync_program}" ${macports_rsync_options} 'src/' 'dst/' 2>&1)

set -e
# Each implementation words this differently: rsync 2.6.9 says
# "unknown option", openrsync says "unrecognized option", and both
# print a usage summary when they reject an argument.
if printf '%s' "${output}" | grep -i -E -q\
'unrecognized option|unknown option|invalid option|is invalid|^usage: rsync'; then
wlog 'Error' '%s rejected the configured options \047%s\047: %s'\
"${rsync_program}" "${macports_rsync_options}" "${output}"
false
fi
)

assert_that_existing_rsync_options_are_replaced()
(
macports_prefix="$(pwd)"
mkdir -p 'etc/macports'
cat > 'etc/macports/macports.conf' <<'CONFIG'
prefix /opt/local
#rsync_options -rtzv --delete-after
rsync_options -rtzv --delete-after
build_arch arm64
CONFIG
write_rsync_options
set -e
# The other settings are left alone.
grep -q '^prefix' 'etc/macports/macports.conf'
grep -q '^build_arch' 'etc/macports/macports.conf'
# Exactly one rsync_options setting is left, and it is ours.
test "$(grep -c 'rsync_options' 'etc/macports/macports.conf')" -eq 1
grep -q '^rsync_options .*--timeout=' 'etc/macports/macports.conf'
)

testsuite_main\
assert_that_a_successful_synchronisation_is_not_retried\
assert_that_a_failed_synchronisation_is_retried\
assert_that_synchronisation_gives_up_after_the_last_attempt\
assert_that_synchronisation_waits_between_attempts\
assert_that_rsync_options_are_written_when_absent\
assert_that_rsync_options_keep_the_macports_defaults\
assert_that_rsync_accepts_the_configured_options\
assert_that_existing_rsync_options_are_replaced\

# End of file `macports'
Loading