Skip to content
Draft
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
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,8 @@ data class DistroDefinition(
val softwarePackageCommands: Map<String, (sshPort: Int) -> String> = emptyMap(),
val softwarePackageLaunchCommands: Map<String, (sshPort: Int) -> String> = emptyMap(),
val softwarePackageExpectedBinaries: Map<String, List<String>> = emptyMap(),
val softwarePackageVersions: Map<String, Int> = emptyMap()
val softwarePackageVersions: Map<String, Int> = emptyMap(),
val softwarePackageHidden: Set<String> = emptySet()
) {
val expectedSizeMb: Int get() = downloadSizeMb

Expand Down Expand Up @@ -288,7 +289,8 @@ object DistroCatalog {
"openssh_server" to { port ->
"apk update && apk add --no-cache openssh-server openssh ca-certificates && mkdir -p /run/sshd /var/run/sshd /var/empty && ssh-keygen -A 2>/dev/null || true && (sed -i 's/^#\\?PasswordAuthentication.*/PasswordAuthentication yes/' /etc/ssh/sshd_config 2>/dev/null || true) && (sed -i 's/^#\\?PermitRootLogin.*/PermitRootLogin yes/' /etc/ssh/sshd_config 2>/dev/null || true) && echo \"Port $port\" >> /etc/ssh/ssh_config && chmod 600 /etc/ssh/ssh_host_*_key 2>/dev/null || true"
}
)
),
softwarePackageHidden = setOf("initd_service_manager")
)

val ARCH_ARM = DistroDefinition(
Expand Down Expand Up @@ -613,7 +615,8 @@ object DistroCatalog {
),
softwarePackageVersions = mapOf(
"xfce_desktop" to 5
)
),
softwarePackageHidden = setOf("initd_service_manager")
)

val ALL_DISTROS = listOf(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,10 @@ data class SoftwarePackage(
return "NGINX HTTP server listening on port $validPort. Test in your browser or with 'curl http://localhost:$validPort'."
}

private const val INITD_VERSION = "0.0.2"
private const val INITD_SHA256_ARM64 = "0484f46990bf48b7b46223064d79c029d2a6789aa24e7fd7b82efea0e3d4634e"
private const val INITD_SHA256_AMD64 = "0dcd1f33ee224ab1f70d264db9378c434cde9bd8c82abba4009e008c5d85c801"

fun buildSshLaunchCommand(port: Int = 2222): String {
val validPort = if (port in 1..65535) port else 2222
return "(sed -i 's/^Subsystem.*sftp/#&/' /etc/ssh/sshd_config 2>/dev/null || true); mkdir -p /run/sshd /var/run/sshd /var/empty && [ -e /dev/ptmx ] || (mknod -m 666 /dev/ptmx c 5 2 2>/dev/null || ln -s /dev/pts/ptmx /dev/ptmx 2>/dev/null || true) && chmod 666 /dev/ptmx 2>/dev/null || true && ssh-keygen -A 2>/dev/null || true && chmod 755 /etc/ssh /run/sshd /var/run/sshd /var/empty 2>/dev/null || true && (killall -9 sshd 2>/dev/null || true) && /usr/sbin/sshd -p $validPort"
Expand All @@ -111,6 +115,178 @@ data class SoftwarePackage(
return "SSH server listening on port $validPort. Connect via 'ssh <username>@<phone-ip> -p $validPort' using your Linux password."
}

fun buildInitdShim(): String = listOf(
"#!/bin/sh",
"# LinuxOnAndroid shim: initd-backed systemctl with automatic daemon recovery",
"CLIENT=/usr/local/lib/initd/systemctl",
"DAEMON=/usr/local/lib/initd/initd",
"SOCK=/run/initd.sock",
"LOCK=/run/.initd-start.lock",
"LOG=/var/log/initd.log",
"",
"case \"\$1\" in",
" reboot|poweroff|halt)",
" echo \"systemctl \$1 is not supported in LinuxOnAndroid.\" >&2",
" exit 1",
" ;;",
"esac",
"",
"alive() {",
" [ -S \"\$SOCK\" ] && \"\$CLIENT\" list-units >/dev/null 2>&1",
"}",
"",
"clear_stale_lock() {",
" if [ -d \"\$LOCK\" ] && ! find \"\$LOCK\" -maxdepth 0 -newermt '-10 seconds' >/dev/null 2>&1; then",
" rmdir \"\$LOCK\" 2>/dev/null",
" fi",
"}",
"",
"wait_alive() {",
" i=0",
" while [ \"\$i\" -lt 30 ]; do",
" if alive; then return 0; fi",
" sleep 0.1",
" i=\$((i + 1))",
" done",
" return 1",
"}",
"",
"start_daemon() {",
" clear_stale_lock",
" if mkdir \"\$LOCK\" 2>/dev/null; then",
" ( umask 000; nohup \"\$DAEMON\" --socket >\"\$LOG\" 2>&1 & sleep 1; rmdir \"\$LOCK\" 2>/dev/null ) &",
" fi",
" wait_alive",
"}",
"",
"offline() {",
" case \"\$1\" in",
" daemon-reload)",
" exit 0",
" ;;",
" is-enabled)",
" shift",
" rc=0",
" for u in \"\$@\"; do",
" n=\$(basename \"\$u\")",
" en=\"\"",
" for d in /etc/systemd/system/*.wants; do",
" if [ -e \"\$d/\$n\" ]; then en=1; fi",
" done",
" if [ -n \"\$en\" ]; then",
" echo \"enabled\"",
" else",
" echo \"disabled\"",
" rc=1",
" fi",
" done",
" exit \$rc",
" ;;",
" enable)",
" shift",
" for u in \"\$@\"; do",
" case \"\$u\" in",
" *.* ) ;;",
" *) u=\"\$u.service\" ;;",
" esac",
" src=\"\"",
" for b in /etc/systemd/system /lib/systemd/system /usr/lib/systemd/system; do",
" if [ -f \"\$b/\$u\" ]; then src=\"\$b/\$u\"; fi",
" done",
" if [ -z \"\$src\" ]; then",
" echo \"Unit \$u not found.\" >&2",
" exit 1",
" fi",
" want=\$(sed -n '/^\\[Install\\]/,/^\\[/s/^WantedBy=//p' \"\$src\" | head -n 1)",
" if [ -z \"\$want\" ]; then want=\"multi-user.target\"; fi",
" mkdir -p \"/etc/systemd/system/\$want.wants\"",
" ln -sf \"\$src\" \"/etc/systemd/system/\$want.wants/\$u\"",
" done",
" exit 0",
" ;;",
" disable)",
" shift",
" for u in \"\$@\"; do",
" case \"\$u\" in",
" *.* ) ;;",
" *) u=\"\$u.service\" ;;",
" esac",
" for d in /etc/systemd/system/*.wants; do",
" if [ -e \"\$d/\$u\" ]; then rm -f \"\$d/\$u\"; fi",
" done",
" done",
" exit 0",
" ;;",
" esac",
"}",
"",
"if alive; then",
" exec \"\$CLIENT\" \"\$@\"",
"fi",
"",
"start_daemon",
"",
"if alive; then",
" exec \"\$CLIENT\" \"\$@\"",
"fi",
"",
"offline \"\$@\"",
"",
"echo \"initd service manager is not running and 'systemctl \$1' requires the daemon.\" >&2",
"exit 1"
).joinToString("\n")

fun buildInitdAutostartHook(): String = listOf(
"#!/bin/sh",
"# LinuxOnAndroid: auto-start the initd service manager when a session opens",
"INITD_DAEMON=/usr/local/lib/initd/initd",
"INITD_CLIENT=/usr/local/lib/initd/systemctl",
"INITD_LOCK=/run/.initd-start.lock",
"if [ -x \"\$INITD_DAEMON\" ]; then",
" if ! \"\$INITD_CLIENT\" list-units >/dev/null 2>&1; then",
" if [ -d \"\$INITD_LOCK\" ] && ! find \"\$INITD_LOCK\" -maxdepth 0 -newermt '-10 seconds' >/dev/null 2>&1; then",
" rmdir \"\$INITD_LOCK\" 2>/dev/null",
" fi",
" if mkdir \"\$INITD_LOCK\" 2>/dev/null; then",
" ( umask 000; nohup \"\$INITD_DAEMON\" --socket >/var/log/initd.log 2>&1 & sleep 1; rmdir \"\$INITD_LOCK\" 2>/dev/null ) &",
" fi",
" fi",
" unset INITD_DAEMON INITD_CLIENT INITD_LOCK",
"fi"
).joinToString("\n")

fun buildInitdInstallCommand(): String {
val shim = buildInitdShim()
val hook = buildInitdAutostartHook()
return listOf(
"$NONINT_EXPORT && dpkg --configure -a && \\",
"{ command -v curl >/dev/null 2>&1 || { apt-get update -qq && apt-get install -y $DPKG_FLAGS curl ca-certificates; }; } && \\",
"{ command -v python3 >/dev/null 2>&1 || { echo 'ERROR: python3 is required to unpack the initd package.' >&2; exit 1; }; } && \\",
"INITD_ARCH=\$(uname -m) && \\",
"INITD_VERSION=$INITD_VERSION && \\",
"if [ \"\$INITD_ARCH\" = \"aarch64\" ] || [ \"\$INITD_ARCH\" = \"arm64\" ]; then INITD_ARCH=arm64; INITD_SHA=$INITD_SHA256_ARM64; elif [ \"\$INITD_ARCH\" = \"x86_64\" ]; then INITD_ARCH=amd64; INITD_SHA=$INITD_SHA256_AMD64; else echo \"ERROR: initd requires arm64 or x86_64 (detected: \$INITD_ARCH).\" >&2; exit 1; fi && \\",
"rm -rf /tmp/initd-pkg /tmp/initd-pkg.zip && mkdir -p /tmp/initd-pkg /usr/local/lib/initd /var/log && \\",
"curl -fSL --retry 3 --connect-timeout 20 -o /tmp/initd-pkg.zip \"https://github.com/EdwardLab/initd/releases/download/$INITD_VERSION/initd-v$INITD_VERSION-linux-\${INITD_ARCH}.zip\" && \\",
"echo \"\$INITD_SHA /tmp/initd-pkg.zip\" | sha256sum -c - && \\",
"python3 -m zipfile -e /tmp/initd-pkg.zip /tmp/initd-pkg && \\",
"[ -f /tmp/initd-pkg/initd ] && [ -f /tmp/initd-pkg/systemctl ] && \\",
"install -m 755 /tmp/initd-pkg/initd /usr/local/lib/initd/initd && \\",
"install -m 755 /tmp/initd-pkg/systemctl /usr/local/lib/initd/systemctl && \\",
"{ [ ! -e /usr/bin/systemctl ] || { [ -e /usr/bin/systemctl.loa-stub ] || mv /usr/bin/systemctl /usr/bin/systemctl.loa-stub; }; } && \\",
"ln -sf /usr/local/bin/systemctl /usr/bin/systemctl && \\",
"cat << 'SHIMEOF' > /usr/local/bin/systemctl",
shim,
"SHIMEOF",
"chmod 755 /usr/local/bin/systemctl && \\",
"cat << 'HOOKEOF' > /etc/profile.d/00-initd-autostart.sh",
hook,
"HOOKEOF",
"chmod 644 /etc/profile.d/00-initd-autostart.sh && \\",
"rm -rf /tmp/initd-pkg /tmp/initd-pkg.zip && \\",
"echo \"initd v$INITD_VERSION (\$INITD_ARCH) installed successfully.\""
).joinToString("\n")
}

fun getPresets(sshPort: Int = 2222): List<SoftwarePackage> {
val validPort = if (sshPort in 1..65535) sshPort else 2222
return listOf(
Expand Down Expand Up @@ -194,6 +370,24 @@ data class SoftwarePackage(
postInstallNotes = buildSshPostInstallNotes(validPort),
expectedBinaries = listOf("usr/sbin/sshd", "usr/bin/ssh-keygen"),
version = 3
),
SoftwarePackage(
id = "initd_service_manager",
name = "initd Service Manager",
category = SoftwareCategory.UTILITIES,
description = "Lightweight systemd-compatible init system. Enables systemctl start/stop/enable for services in proot where systemd cannot run.",
iconName = "Settings",
installCommand = buildInitdInstallCommand(),
launchCommand = "systemctl list-units",
postInstallNotes = "The service manager auto-starts with each session. Manage services with 'systemctl start/stop/enable <unit>'. 'systemctl reboot/poweroff/halt' is disabled.",
expectedBinaries = listOf(
"usr/local/lib/initd/initd",
"usr/local/lib/initd/systemctl",
"usr/local/bin/systemctl",
"usr/bin/systemctl",
"etc/profile.d/00-initd-autostart.sh"
),
version = 1
)
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -685,7 +685,9 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
val packageVersions = if (installed) RootfsMigrationManager.readPackageVersions(rootfsDir) else emptyMap()
val distroDef = com.devwithzachary.completelinuxinstaller.model.DistroCatalog.getById(container.distroId.ifBlank { container.distroName })

return _packages.value.map { pkg ->
return _packages.value
.filter { pkg -> pkg.id !in distroDef.softwarePackageHidden }
.map { pkg ->
val effectiveLaunchCommand = distroDef.getSoftwarePackageLaunchCommand(pkg.id, _sshPort.value) ?: pkg.launchCommand
val effectiveBinaries = distroDef.getSoftwarePackageExpectedBinaries(pkg.id) ?: pkg.expectedBinaries
val effectiveVersion = distroDef.getSoftwarePackageVersion(pkg.id) ?: pkg.version
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ fun SoftwareCard(
"Terminal" -> Icons.Default.Terminal
"Security" -> Icons.Default.Security
"Android" -> Icons.Default.Android
"Settings" -> Icons.Default.Settings
else -> Icons.Default.Apps
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ class SoftwarePackageTest {
fun testGetPresets_returnsNonEmptyList() {
val presets = SoftwarePackage.getPresets()
assertTrue("Preset package list should not be empty", presets.isNotEmpty())
assertEquals(6, presets.size)
assertEquals(7, presets.size)
}

@Test
Expand Down Expand Up @@ -156,4 +156,52 @@ class SoftwarePackageTest {
tempDir.deleteRecursively()
}
}

@Test
fun testPreset_initd_service_manager_pinsVersionAndHashes() {
val initd = SoftwarePackage.getPresets().find { it.id == "initd_service_manager" }
assertNotNull("initd_service_manager preset must exist", initd)
initd?.let {
assertTrue("installCommand must pin the release tag and asset version", it.installCommand.contains("releases/download/0.0.2/initd-v0.0.2-linux-"))
assertTrue("installCommand must contain the amd64 SHA256 hash", it.installCommand.contains("0dcd1f33ee224ab1f70d264db9378c434cde9bd8c82abba4009e008c5d85c801"))
assertTrue("installCommand must contain the arm64 SHA256 hash", it.installCommand.contains("0484f46990bf48b7b46223064d79c029d2a6789aa24e7fd7b82efea0e3d4634e"))
assertTrue("installCommand must verify SHA256 with sha256sum -c", it.installCommand.contains("sha256sum -c"))
assertTrue("installCommand must install initd to /usr/local/lib/initd/initd", it.installCommand.contains("/usr/local/lib/initd/initd"))
assertTrue("installCommand must install systemctl client to /usr/local/lib/initd/systemctl", it.installCommand.contains("/usr/local/lib/initd/systemctl"))
assertTrue("installCommand must install autostart hook to /etc/profile.d/", it.installCommand.contains("/etc/profile.d/00-initd-autostart.sh"))
assertTrue("installCommand must guard armv7 with uname -m arch check", it.installCommand.contains("uname -m"))
assertTrue("installCommand must reject non-arm64/amd64 architectures", it.installCommand.contains("x86_64"))
assertTrue("installCommand must use python3 to extract the zip (unzip absent in ubuntu base)", it.installCommand.contains("python3 -m zipfile"))
assertTrue("launchCommand must list units", it.launchCommand != null && it.launchCommand.contains("list-units"))
assertEquals(
"expectedBinaries must match installed artifacts",
setOf(
"usr/local/lib/initd/initd",
"usr/local/lib/initd/systemctl",
"usr/local/bin/systemctl",
"usr/bin/systemctl",
"etc/profile.d/00-initd-autostart.sh"
),
it.expectedBinaries.toSet()
)
assertEquals(1, it.version)
}
}

@Test
fun testPreset_initd_shimInterceptsReboot() {
val shim = SoftwarePackage.buildInitdShim()
assertTrue("shim must intercept reboot", shim.contains("reboot|poweroff|halt"))
assertFalse("shim must be POSIX sh (no bashisms)", shim.contains("[["))
assertTrue("shim must be POSIX sh shebang", shim.startsWith("#!/bin/sh"))
}

@Test
fun testBuildInitdInstallCommand_containsDpkgAndPolicyRcD() {
val cmd = SoftwarePackage.buildInitdInstallCommand()
assertTrue("initd install must contain dpkg --configure -a", cmd.contains("dpkg --configure -a"))
assertTrue("initd install must contain policy-rc.d", cmd.contains("policy-rc.d"))
assertTrue("initd install must contain apt-get install -y for curl fallback", cmd.contains("apt-get install -y"))
assertTrue("initd install must contain messagebus system user initialization", cmd.contains("messagebus:"))
}
}