3dfx only for arm64 and x86_64

This commit is contained in:
NOURELDEEN ELSAYED 2026-10-08 22:44:35 +03:00
parent 54111c24b6
commit 105d47b7d7
4 changed files with 144 additions and 16 deletions

View file

@ -37,7 +37,7 @@ Vectras VM is a QEMU-based virtual machine app for Android. Create and manage vi
- Import and export `.cvbi` ROM packages, or use your own disk images and installation media.
- Browse the ROM Store and Software Store for downloadable content.
- Use the bundled bootstrap and Alpine Linux environment through PROOT, with an integrated terminal.
- Choose an online QEMU package or supply a compatible local archive during setup.
- Choose an online QEMU package or supply a compatible local archive when available.
- Use supported graphics options, including 3dfx with a suitable QEMU build and guest configuration.
The QEMU version depends on the package you install; it is not fixed by the APK version.
@ -101,7 +101,9 @@ An alternative download listing is available on [OpenAPK](https://www.openapk.ne
## 🚀 First-time setup
The Community Version uses the bootstrap and Alpine Linux environment bundled in the APK. The app extracts this environment during setup; the QEMU package is selected separately.
The Community Version uses the bootstrap and Alpine Linux environment bundled in the APK. The app extracts this environment during setup, then installs QEMU using the path for your device architecture.
**Our official 3dfx-patched QEMU builds are currently available only for ARM64 (`arm64-v8a`) and x86_64.** ARMv7 (`armeabi-v7a`) and 32-bit x86 use standard official QEMU packages from Alpine, without the 3dfx modification, when their archive links in `setupfiles.json` are empty.
When prompted, choose one of the two QEMU setup options:

View file

@ -0,0 +1,30 @@
package com.vectras.vm.setupwizard;
/** Selects the QEMU source for the ABI of the bundled Alpine environment. */
final class QemuSetupPolicy {
// apk resolves the matching firmware, shared libraries, and module dependencies.
static final String ALPINE_QEMU_PACKAGES = "qemu qemu-img qemu-tools qemu-modules"
+ " qemu-system-i386 qemu-system-x86_64 qemu-system-aarch64 qemu-system-ppc"
// Provides vncpasswd for the existing VNC setup step.
+ " tigervnc";
private QemuSetupPolicy() {
}
static boolean is32BitAbi(String abi) {
return "armeabi-v7a".equals(abi) || "armeabi".equals(abi) || "x86".equals(abi);
}
static boolean usesAlpinePackages(String abi, String manifestUrl) {
// Missing/invalid metadata or a failed request must not count as an empty link.
return is32BitAbi(abi) && manifestUrl != null && manifestUrl.trim().isEmpty();
}
static String manifestKey(String abi) {
if ("arm64-v8a".equals(abi)) return "aarch64";
if ("armeabi-v7a".equals(abi) || "armeabi".equals(abi)) return "armhf";
if ("x86_64".equals(abi)) return "amd64";
if ("x86".equals(abi)) return "x86";
throw new IllegalArgumentException("Unsupported QEMU setup ABI: " + abi);
}
}

View file

@ -72,6 +72,7 @@ public class SetupWizard2Activity extends AppCompatActivity {
String progressText ="0%";
String qemuDownloadUrl = "";
String qemuDownloadSha256 = "";
boolean installQemuFromAlpinePackages = false;
boolean isExecutingCommand = false;
boolean isLibProotError = false;
boolean aria2Error = false;
@ -375,6 +376,16 @@ public class SetupWizard2Activity extends AppCompatActivity {
}
private void requestQemuArchive() {
if (isExecutingCommand) return;
installQemuFromAlpinePackages = false;
if (QemuSetupPolicy.is32BitAbi(Build.SUPPORTED_ABIS[0])) {
loadQemuManifest(true);
return;
}
showQemuArchiveDialog();
}
private void showQemuArchiveDialog() {
uiController(STEP_SETUP_OPTIONS);
binding.standardSetupOption.setVisibility(View.VISIBLE);
DialogUtils.twoDialog(this,
@ -389,22 +400,30 @@ public class SetupWizard2Activity extends AppCompatActivity {
}
private void getDataForStandardSetup() {
loadQemuManifest(false);
}
private void loadQemuManifest(boolean showArchiveOptions) {
if (isExecutingCommand) return;
isExecutingCommand = true;
uiController(STEP_GETTING_DATA);
qemuDownloadUrl = "";
qemuDownloadSha256 = "";
installQemuFromAlpinePackages = false;
Retrofit2Utils.get(AppConfig.bootstrapfileslink, (isSuccess, body, status, error) -> {
isExecutingCommand = false;
if (isFinishing() || isDestroyed()) return;
if (isSuccess) {
try {
JsonObject manifest = JsonParser.parseString(body).getAsJsonObject();
String key = DeviceUtils.isArm()
? (DeviceUtils.is64bit() ? "aarch64" : "armhf")
: (DeviceUtils.is64bit() ? "amd64" : "x86");
String abi = Build.SUPPORTED_ABIS[0];
String key = QemuSetupPolicy.manifestKey(abi);
if (manifest.has(key) && !manifest.get(key).isJsonNull()) {
String url = manifest.get(key).getAsString();
if (!manifest.get(key).isJsonPrimitive() || !manifest.get(key).getAsJsonPrimitive().isString()) {
throw new IllegalArgumentException("Invalid QEMU URL field in setup JSON");
}
String url = manifest.get(key).getAsString().trim();
installQemuFromAlpinePackages = QemuSetupPolicy.usesAlpinePackages(abi, url);
// No release URL is hard-coded: the repository JSON selects the archive.
if (url.startsWith("https://") && Uri.parse(url).getHost() != null) {
qemuDownloadUrl = url;
@ -421,12 +440,19 @@ public class SetupWizard2Activity extends AppCompatActivity {
} catch (Exception e) {
qemuDownloadUrl = "";
qemuDownloadSha256 = "";
installQemuFromAlpinePackages = false;
Log.e("SetupWizard2Activity", "Invalid QEMU setup JSON", e);
}
} else {
Log.e("SetupWizard2Activity", "Could not load QEMU setup JSON: " + status, error);
}
if (!qemuDownloadUrl.isEmpty()) {
if (installQemuFromAlpinePackages) {
isCustomSetupMode = false;
startSetup();
} else if (showArchiveOptions) {
// Keep manual selection available even when the manifest request fails.
showQemuArchiveDialog();
} else if (!qemuDownloadUrl.isEmpty()) {
isCustomSetupMode = false;
startSetup();
} else {
@ -438,7 +464,10 @@ public class SetupWizard2Activity extends AppCompatActivity {
private void startSetup() {
if (isExecutingCommand) return;
if (!isCustomSetupMode && qemuDownloadUrl.isEmpty()) {
final boolean installQemuFromPackages = installQemuFromAlpinePackages
&& QemuSetupPolicy.is32BitAbi(Build.SUPPORTED_ABIS[0]);
if (installQemuFromPackages) isCustomSetupMode = false;
if (!installQemuFromPackages && !isCustomSetupMode && qemuDownloadUrl.isEmpty()) {
requestQemuArchive();
return;
}
@ -450,7 +479,7 @@ public class SetupWizard2Activity extends AppCompatActivity {
final String downloadQemuCommand;
try {
checkQemuCommand = SetupFeatureCore.readTextFromAssets(this, "setup/check-qemu.sh");
downloadQemuCommand = isCustomSetupMode ? "" :
downloadQemuCommand = installQemuFromPackages || isCustomSetupMode ? "" :
SetupFeatureCore.readTextFromAssets(this, "setup/download-qemu.sh");
} catch (Exception e) {
isExecutingCommand = false;
@ -484,11 +513,11 @@ public class SetupWizard2Activity extends AppCompatActivity {
" echo \"Starting setup...\";" +
" apk update;" +
" echo \"Installing packages...\";" +
" apk add " + (DeviceUtils.is64bit() ? AppConfig.neededPkgs()
: AppConfig.neededPkgs32bit()) + ";";
" apk add " + (QemuSetupPolicy.is32BitAbi(Build.SUPPORTED_ABIS[0]) ? AppConfig.neededPkgs32bit()
: AppConfig.neededPkgs()) + ";";
String qemuArchivePath = isCustomSetupMode ? tarPath : "/root/setup.tar.gz";
if (!isCustomSetupMode) {
if (!installQemuFromPackages && !isCustomSetupMode) {
// Verify the complete download before removing an installed QEMU during updates.
cmd += " set -- " + shellQuote(qemuDownloadUrl) + " " +
shellQuote(qemuDownloadSha256) + ";\n" + downloadQemuCommand + "\n";
@ -517,10 +546,17 @@ public class SetupWizard2Activity extends AppCompatActivity {
" set -e;";
}
cmd += " echo \"Installing Qemu...\";" +
" tar -xzvf " + shellQuote(qemuArchivePath) + " -C /;" +
" rm " + shellQuote(qemuArchivePath) + ";" +
" chmod 755 /usr/local/bin/*;";
cmd += " echo \"Installing Qemu...\";";
if (installQemuFromPackages) {
cmd += " echo \"Installing QEMU from Alpine packages...\";" +
" apk add --no-cache " + QemuSetupPolicy.ALPINE_QEMU_PACKAGES + ";" +
" for binary in qemu-system-i386 qemu-system-x86_64 qemu-system-aarch64 qemu-system-ppc qemu-img; do " +
" /usr/bin/\"$binary\" --version || exit $?; done;";
} else {
cmd += " tar -xzvf " + shellQuote(qemuArchivePath) + " -C /;" +
" rm " + shellQuote(qemuArchivePath) + ";" +
" chmod 755 /usr/local/bin/*;";
}
if (ACTION != ACTION_SYSTEM_UPDATE) {
cmd += " echo \"Just a sec...\";" +
" mkdir -p ~/.vnc && echo -e \"555555\\n555555\" | vncpasswd -f > ~/.vnc/passwd && chmod 0600 ~/.vnc/passwd;";
@ -547,6 +583,7 @@ public class SetupWizard2Activity extends AppCompatActivity {
try {
FileUtils.copyFileFromUri(this, uri, tarPath);
runOnUiThread(() -> {
installQemuFromAlpinePackages = false;
isCustomSetupMode = true;
startSetup();
});

View file

@ -0,0 +1,59 @@
package com.vectras.vm.setupwizard;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
import org.junit.Test;
public class QemuSetupPolicyTest {
@Test
public void arm32AndX86UseAlpinePackagesOnlyForEmptyLinks() {
assertTrue(QemuSetupPolicy.usesAlpinePackages("armeabi-v7a", ""));
assertTrue(QemuSetupPolicy.usesAlpinePackages("armeabi", ""));
assertTrue(QemuSetupPolicy.usesAlpinePackages("x86", " "));
}
@Test
public void sixtyFourBitAndUnknownAbisKeepArchiveSetup() {
assertFalse(QemuSetupPolicy.usesAlpinePackages("arm64-v8a", ""));
assertFalse(QemuSetupPolicy.usesAlpinePackages("x86_64", ""));
assertFalse(QemuSetupPolicy.usesAlpinePackages("riscv64", ""));
assertFalse(QemuSetupPolicy.usesAlpinePackages(null, ""));
}
@Test
public void populatedMissingAndInvalidLinksNeverUseAlpinePackages() {
for (String abi : new String[]{"armeabi-v7a", "armeabi", "x86", "arm64-v8a", "x86_64"}) {
assertFalse(QemuSetupPolicy.usesAlpinePackages(abi, "https://example.com/qemu.tar.gz"));
assertFalse(QemuSetupPolicy.usesAlpinePackages(abi, null));
assertFalse(QemuSetupPolicy.usesAlpinePackages(abi, "not-a-url"));
}
}
@Test
public void abiMapsToTheCorrectManifestKey() {
org.junit.Assert.assertEquals("aarch64", QemuSetupPolicy.manifestKey("arm64-v8a"));
org.junit.Assert.assertEquals("armhf", QemuSetupPolicy.manifestKey("armeabi-v7a"));
org.junit.Assert.assertEquals("armhf", QemuSetupPolicy.manifestKey("armeabi"));
org.junit.Assert.assertEquals("amd64", QemuSetupPolicy.manifestKey("x86_64"));
org.junit.Assert.assertEquals("x86", QemuSetupPolicy.manifestKey("x86"));
}
@Test(expected = IllegalArgumentException.class)
public void unknownAbiDoesNotSelectAnUnrelatedDownload() {
QemuSetupPolicy.manifestKey("riscv64");
}
@Test
public void packageListIncludesAllAppEnginesToolsAndModules() {
Set<String> packages = new HashSet<>(Arrays.asList(QemuSetupPolicy.ALPINE_QEMU_PACKAGES.split(" ")));
for (String required : new String[]{"qemu", "qemu-img", "qemu-tools", "qemu-modules",
"qemu-system-i386", "qemu-system-x86_64", "qemu-system-aarch64", "qemu-system-ppc", "tigervnc"}) {
assertTrue("Missing package: " + required, packages.contains(required));
}
}
}