ci: build and test on root self-hosted runners (#2237)

This commit is contained in:
Kurt Himebauch authored and GitHub committed 2026-11-07 17:02:19 -05:00
1 parent 8fa30b99f9
commit d29a1a2dc2
10 files changed
+59 -5

No files matched your search

@@ -0,0 +1,16 @@
package com.droiddeck.launcher
import java.nio.file.Files
import org.junit.Assume.assumeTrue
/** Root ignores file mode bits, so tests that rely on a denied read or write cannot run as root or without POSIX permissions. */
fun assumeFilePermissionsEnforced() {
val probe = Files.createTempDirectory("permission-probe").toFile()
try {
assumeTrue("no POSIX file permissions", Files.getFileStore(probe.toPath()).supportsFileAttributeView("posix"))
assumeTrue("file permissions are not enforced for this user", probe.setReadable(false, false) && !probe.canRead())
} finally {
probe.setReadable(true, false)
probe.delete()
}
}
@@ -1,5 +1,6 @@
package com.droiddeck.launcher.files
import com.droiddeck.launcher.assumeFilePermissionsEnforced
import java.io.File
import java.util.concurrent.CancellationException
import java.nio.file.Files
@@ -114,6 +115,7 @@ class FileOpsCopyTest {
}
@Test fun anOverwriteStillRefusesAReadOnlyDestination() {
assumeFilePermissionsEnforced()
assumeTrue(Files.getFileStore(tmp.root.toPath()).supportsFileAttributeView("posix"))
val src = tmp.newFile("source").apply { writeText("replacement") }
val dst = tmp.newFile("dest").apply { writeText("original") }
@@ -1,5 +1,6 @@
package com.droiddeck.launcher.frontend
import com.droiddeck.launcher.assumeFilePermissionsEnforced
import android.os.Environment
import com.droiddeck.launcher.session.SessionPrefs
import java.io.File
@@ -308,6 +309,7 @@ class AddedExesTest {
}
@Test fun anExeInAFolderTheAppCannotReadIsNotAdded() {
assumeFilePermissionsEnforced()
noGamesFolders()
val outside = exe(File(tmp.newFolder("elsewhere"), "Game/Game.exe"))
outside.parentFile!!.setReadable(false)
@@ -1,5 +1,6 @@
package com.droiddeck.launcher.runtime
import com.droiddeck.launcher.assumeFilePermissionsEnforced
import java.io.File
import java.nio.file.Files
import java.nio.file.LinkOption.NOFOLLOW_LINKS
@@ -128,6 +129,7 @@ class RuntimeRemovalTest {
}
@Test fun aPartialRemovalStaysQuarantinedAndRetryable() {
assumeFilePermissionsEnforced()
val root = runtime()
try {
val removal = LinuxRuntimeInstaller.beginUninstall(null, root)!!
@@ -146,6 +148,7 @@ class RuntimeRemovalTest {
}
@Test fun reportsFailureWhenTheRuntimeCannotBeQuarantinedAndAllowsRetry() {
assumeFilePermissionsEnforced()
val root = runtime()
Files.setPosixFilePermissions(dir.toPath(), PosixFilePermissions.fromString("r-xr-xr-x"))
try {
@@ -1,5 +1,6 @@
package com.droiddeck.launcher.runtime
import com.droiddeck.launcher.assumeFilePermissionsEnforced
import java.io.File
import java.io.IOException
import java.nio.file.Files
@@ -155,6 +156,7 @@ class RuntimeUserDataCarryTest {
}
@Test fun anUnreadableAppImageDirectoryAbortsBeforeMovingHome() {
assumeFilePermissionsEnforced()
assumeTrue(Files.getFileStore(tmp.root.toPath()).supportsFileAttributeView("posix"))
val old = userTree()
val images = File(old, "opt/appimages")
@@ -264,6 +266,7 @@ class RuntimeUserDataCarryTest {
}
@Test fun anInaccessibleLiveHomeIsNeverTreatedAsMissing() {
assumeFilePermissionsEnforced()
assumeTrue(Files.getFileStore(tmp.root.toPath()).supportsFileAttributeView("posix"))
val root = tmp.newFolder("runtime")
val staging = tmp.newFolder("runtime.new")
@@ -1,5 +1,6 @@
package com.droiddeck.launcher.session
import com.droiddeck.launcher.assumeFilePermissionsEnforced
import android.content.Intent
import org.json.JSONObject
import org.junit.Assert.assertEquals
@@ -403,6 +404,7 @@ class EnhancedSessionLogShareTest {
}
@Test fun unreadableDiscoverySubtreeKeepsOtherLogsAndMarksTheManifestPartial() {
assumeFilePermissionsEnforced()
assumeFalse(System.getProperty("os.name").startsWith("Windows"))
val session = tmp.newFolder("2026-11-04-12-partial-discovery")
session.resolve("session.log").writeText("safe\n")
+9 -4
View File
@@ -10,9 +10,13 @@ if [ "${#missing[@]}" = 0 ]; then
echo "Ubuntu build dependencies are already installed."
exit 0
fi
options=(
-o Dir::Etc::sourcelist=sources.list.d/ubuntu.sources
-o Dir::Etc::sourceparts=-
options=()
# Runners without the deb822 Ubuntu sources file keep their own apt configuration.
ubuntu_sources=${UBUNTU_SOURCES_FILE:-/etc/apt/sources.list.d/ubuntu.sources}
if [ -f "$ubuntu_sources" ]; then
options+=(-o "Dir::Etc::sourcelist=$ubuntu_sources" -o Dir::Etc::sourceparts=-)
fi
options+=(
-o Acquire::Retries=1
-o Acquire::http::Timeout=15
-o Acquire::https::Timeout=15
@@ -28,13 +32,14 @@ if install_packages; then
exit 0
fi
echo "::warning::Runner mirror failed or timed out; retrying with the official Ubuntu mirror."
[ -f "$ubuntu_sources" ] || exit 1
sources=$(mktemp --suffix=.sources)
trap 'rm -f "$sources"' EXIT
mirror=https://archive.ubuntu.com/ubuntu
case "$(dpkg --print-architecture)" in
arm64) mirror=https://ports.ubuntu.com/ubuntu-ports ;;
esac
sed -E "s@^URIs:.*@URIs: $mirror@" /etc/apt/sources.list.d/ubuntu.sources > "$sources"
sed -E "s@^URIs:.*@URIs: $mirror@" "$ubuntu_sources" > "$sources"
chmod 644 "$sources"
options[1]="Dir::Etc::sourcelist=$sources"
install_packages
+3
View File
@@ -139,6 +139,9 @@
"app/src/main/jniLibs/arm64-v8a/libproot.so",
"app/src/main/jniLibs/arm64-v8a/libproot-loader.so"
],
"packages": [
"gawk"
],
"ndk": true
},
"comet": {
+7
View File
@@ -52,6 +52,13 @@ cp "$HERE/talloc-config.h" "$WORK/talloc/config/config.h"
"$TOOLCHAIN/llvm-ar" rcs "$WORK/libtalloc.a" "$WORK/talloc.o"
mkdir -p "$WORK/bin"
ln -s "$TOOLCHAIN/llvm-readelf" "$WORK/bin/readelf"
# loader-info.awk needs strtonum, which mawk (the default awk on some runners) lacks.
if command -v gawk >/dev/null; then
ln -s "$(command -v gawk)" "$WORK/bin/awk"
elif ! awk 'BEGIN { exit !(strtonum("0x10") == 16) }' 2>/dev/null; then
echo "gawk, or an awk with strtonum, is required to build the proot loader" >&2
exit 1
fi
PATH="$WORK/bin:$PATH" make -C "$WORK/proot/src" -j"$(getconf _NPROCESSORS_ONLN)" \
CC="$CC" STRIP="$TOOLCHAIN/llvm-strip" OBJCOPY="$TOOLCHAIN/llvm-objcopy" OBJDUMP="$TOOLCHAIN/llvm-objdump" \
CPPFLAGS="-D_FILE_OFFSET_BITS=64 -D_GNU_SOURCE -I. -I$WORK/talloc -DARG_MAX=131072 -Wno-error=implicit-function-declaration" \
+12 -1
View File
@@ -64,7 +64,10 @@ class CiChecksTest(unittest.TestCase):
query.write_text('#!/bin/sh\nexit 1\n')
query.chmod(0o755)
log = root / 'apt.log'
env = dict(os.environ, PATH=str(root) + os.pathsep + os.environ['PATH'], APT_LOG=str(log))
sources = root / 'ubuntu.sources'
sources.write_text('URIs: http://mirror.invalid/ubuntu\n')
env = dict(os.environ, PATH=str(root) + os.pathsep + os.environ['PATH'], APT_LOG=str(log),
UBUNTU_SOURCES_FILE=str(sources))
installer = ROOT / 'tools/ci/install-ubuntu-packages.sh'
result = subprocess.run(['bash', str(installer), 'qemu-user'], env=dict(env, APT_RESULT='17'))
self.assertEqual(result.returncode, 17)
@@ -74,11 +77,19 @@ class CiChecksTest(unittest.TestCase):
update, install = log.read_text().splitlines()
for command in (update, install):
self.assertIn('Dir::Etc::sourceparts=-', command)
self.assertIn(f'Dir::Etc::sourcelist={sources}', command)
self.assertIn('Acquire::https::Timeout=15', command)
self.assertIn('Acquire::Retries=1', command)
self.assertIn('APT::Update::Error-Mode=any', update)
self.assertIn('install -y -q --no-install-recommends qemu-user', install)
log.unlink()
absent = dict(env, UBUNTU_SOURCES_FILE=str(root / 'missing.sources'), APT_RESULT='17')
result = subprocess.run(['bash', str(installer), 'qemu-user'], env=absent)
self.assertEqual(result.returncode, 1)
self.assertEqual(len(log.read_text().splitlines()), 1)
self.assertNotIn('Dir::Etc::sourceparts', log.read_text())
def test_required_status_rejects_failed_cancelled_or_skipped_test_jobs(self):
jobs = yaml.safe_load((ROOT / '.github/workflows/build.yml').read_text())['jobs']
gate = jobs['checks']