Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat[downloader]: download and extract native libraries #6629

Merged
merged 2 commits into from
Feb 20, 2025
Merged
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 @@ -325,6 +325,14 @@ public static void launchMinecraft(final AppCompatActivity activity, MinecraftAc
}
javaArgList.add("-Dlog4j.configurationFile=" + configFile);
}

File versionSpecificNativesDir = new File(Tools.DIR_CACHE, "natives/"+versionId);
if(versionSpecificNativesDir.exists()) {
String dirPath = versionSpecificNativesDir.getAbsolutePath();
javaArgList.add("-Djava.library.path="+dirPath+":"+Tools.NATIVE_LIB_DIR);
javaArgList.add("-Djna.boot.library.path="+dirPath);
}

javaArgList.addAll(Arrays.asList(getMinecraftJVMArgs(versionId, gamedir)));
javaArgList.add("-cp");
javaArgList.add(launchClassPath + ":" + getLWJGL3ClassPath());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,10 @@
public class MinecraftDownloader {
private static final double ONE_MEGABYTE = (1024d * 1024d);
public static final String MINECRAFT_RES = "https://resources.download.minecraft.net/";
private static final String MAVEN_CENTRAL_REPO1 = "https://repo1.maven.org/maven2/";
private AtomicReference<Exception> mDownloaderThreadException;
private ArrayList<DownloaderTask> mScheduledDownloadTasks;
private ArrayList<File> mDeclaredNatives;
private AtomicLong mProcessedFileCounter;
private AtomicLong mProcessedSizeCounter; // Total bytes of processed files (passed SHA1 or downloaded)
private AtomicLong mInternetUsageCounter; // How many bytes downloaded over Internet
Expand Down Expand Up @@ -88,6 +90,7 @@ private void downloadGame(Activity activity, JMinecraftVersionList.Version verIn

mTargetJarFile = createGameJarPath(versionName);
mScheduledDownloadTasks = new ArrayList<>();
mDeclaredNatives = new ArrayList<>();
mProcessedFileCounter = new AtomicLong(0);
mProcessedSizeCounter = new AtomicLong(0);
mInternetUsageCounter = new AtomicLong(0);
Expand Down Expand Up @@ -120,6 +123,7 @@ private void downloadGame(Activity activity, JMinecraftVersionList.Version verIn
throw thrownException;
} else {
ensureJarFileCopy();
extractNatives(versionName);
}
}catch (InterruptedException e) {
// Interrupted while waiting, which means that the download was cancelled.
Expand Down Expand Up @@ -167,6 +171,25 @@ private void ensureJarFileCopy() throws IOException {
org.apache.commons.io.FileUtils.copyFile(mSourceJarFile, mTargetJarFile, false);
}

private void extractNatives(String versionName) throws IOException {
if(mDeclaredNatives.isEmpty()) return;
int totalCount = mDeclaredNatives.size();

ProgressLayout.setProgress(ProgressLayout.DOWNLOAD_MINECRAFT, 0,
R.string.newdl_extracting_native_libraries, 0, totalCount);

File targetDirectory = new File(Tools.DIR_CACHE, "natives/"+versionName);
FileUtils.ensureDirectory(targetDirectory);
NativesExtractor nativesExtractor = new NativesExtractor(targetDirectory);
int extractedCount = 0;
for(File source : mDeclaredNatives) {
nativesExtractor.extractFromAar(source);
extractedCount++;
ProgressLayout.setProgress(ProgressLayout.DOWNLOAD_MINECRAFT, extractedCount * 100 / totalCount,
R.string.newdl_extracting_native_libraries, extractedCount, totalCount);
}
}

private File downloadGameJson(JMinecraftVersionList.Version verInfo) throws IOException, MirrorTamperedException {
File targetFile = createGameJsonPath(verInfo.id);
if(verInfo.sha1 == null && targetFile.canRead() && targetFile.isFile())
Expand Down Expand Up @@ -274,13 +297,31 @@ private void scheduleDownload(File targetFile, int downloadClass, String url, St
);
}

/**
* Schedule the download of an AAR library containing the required natives, for later extraction
* and adding to the library path.
* @param baseRepository the source Maven repository to download from.
* @param dependentLibrary the DependentLibrary to get the path from
* @throws IOException in case if download scheduling fails.
*/
private void scheduleNativeLibraryDownload(String baseRepository, DependentLibrary dependentLibrary) throws IOException {
String path = FileUtils.removeExtension(Tools.artifactToPath(dependentLibrary)) + ".aar";
String downloadUrl = baseRepository + path;
File targetPath = new File(Tools.DIR_HOME_LIBRARY, path);
mDeclaredNatives.add(targetPath);
scheduleDownload(targetPath, DownloadMirror.DOWNLOAD_CLASS_LIBRARIES, downloadUrl, null, 0, true);
}

private void scheduleLibraryDownloads(DependentLibrary[] dependentLibraries) throws IOException {
Tools.preProcessLibraries(dependentLibraries);
growDownloadList(dependentLibraries.length);
for(DependentLibrary dependentLibrary : dependentLibraries) {
// Don't download lwjgl, we have our own bundled in.
if(dependentLibrary.name.startsWith("org.lwjgl")) continue;

// Special handling for JNA Android natives
if(dependentLibrary.name.startsWith("net.java.dev.jna:jna:")) {
scheduleNativeLibraryDownload(MAVEN_CENTRAL_REPO1, dependentLibrary);
}
String libArtifactPath = Tools.artifactToPath(dependentLibrary);
String sha1 = null, url = null;
long size = 0;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
package net.kdt.pojavlaunch.tasks;

import net.kdt.pojavlaunch.Architecture;
import net.kdt.pojavlaunch.Tools;
import net.kdt.pojavlaunch.utils.FileUtils;

import java.io.File;
import java.io.FileInputStream;
import java.io.FilterInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.zip.CRC32;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;

public class NativesExtractor {
private static final ArrayList<String> LIBRARY_BLACKLIST = createLibraryBlacklist();
private final File mDestinationDir;
private final String mLibraryLocation;

public NativesExtractor(File mDestinationDir) {
this.mDestinationDir = mDestinationDir;
this.mLibraryLocation = "jni/"+getAarArchitectureName()+"/";
}

/**
* Create a library blacklist so that downloaded natives are not able to
* override built-in libraries.
* @return the resulting blacklist of library file names
*/
private static ArrayList<String> createLibraryBlacklist() {
String[] includedLibraryNames = new File(Tools.NATIVE_LIB_DIR).list();
ArrayList<String> blacklist = new ArrayList<>(includedLibraryNames.length);
for(String libraryName : includedLibraryNames) {
// allow overriding jnidispatch (as the integrated version may be too old)
if(libraryName.equals("libjnidispatch.so")) continue;
blacklist.add(libraryName);
}
blacklist.trimToSize();
return blacklist;
}

private static String getAarArchitectureName() {
int architecture = Architecture.getDeviceArchitecture();
switch (architecture) {
case Architecture.ARCH_ARM:
return "armeabi-v7a";
case Architecture.ARCH_ARM64:
return "arm64-v8a";
case Architecture.ARCH_X86:
return "x86";
case Architecture.ARCH_X86_64:
return "x86_64";
}
throw new RuntimeException("Unknown CPU architecture: "+architecture);
}

public void extractFromAar(File source) throws IOException {
byte[] buffer = new byte[8192];
try (FileInputStream fileInputStream = new FileInputStream(source);
ZipInputStream zipInputStream = new ZipInputStream(fileInputStream)) {
// Wrap the ZIP input stream into a non-closeable stream to
// avoid it being closed by processEntry()
NonCloseableInputStream entryCopyStream = new NonCloseableInputStream(zipInputStream);
ZipEntry entry;
while((entry = zipInputStream.getNextEntry()) != null) {
String entryName = entry.getName();
if(!entryName.startsWith(mLibraryLocation) || entry.isDirectory()) continue;
// Entry name is actually the full path, so we need to strip the path before extraction
entryName = FileUtils.getFileName(entryName);
// getFileName may make the file name null, avoid that case.
if(entryName == null || LIBRARY_BLACKLIST.contains(entryName)) continue;

processEntry(entryCopyStream, entry, new File(mDestinationDir, entryName), buffer);
}
}
}

private static long fileCrc32(File target, byte[] buffer) throws IOException {
try(FileInputStream fileInputStream = new FileInputStream(target)) {
CRC32 crc32 = new CRC32();
int len;
while((len = fileInputStream.read(buffer)) != -1) {
crc32.update(buffer, 0, len);
}
return crc32.getValue();
}
}

private void processEntry(InputStream sourceStream, ZipEntry zipEntry, File entryDestination, byte[] buffer) throws IOException {
if(entryDestination.exists()) {
long expectedSize = zipEntry.getSize();
long expectedCrc32 = zipEntry.getCrc();
long realSize = entryDestination.length();
long realCrc32 = fileCrc32(entryDestination, buffer);
// File in archive is the same as the local one, don't extract
if(realSize == expectedSize && realCrc32 == expectedCrc32) return;
}
// copyInputStreamToFile copies the stream to a file and then closes it.
org.apache.commons.io.FileUtils.copyInputStreamToFile(sourceStream, entryDestination);
}


private static class NonCloseableInputStream extends FilterInputStream {

protected NonCloseableInputStream(InputStream in) {
super(in);
}

@Override
public void close() {
// Do nothing (the point of this class)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,17 @@ public static String getFileName(String pathOrUrl) {
return pathOrUrl.substring(lastSlashIndex);
}

/**
* Remove the extension (all text after the last dot) from a path/URL string.
* @param pathOrUrl the path or the URL of the file
* @return the input with the extension removed
*/
public static String removeExtension(String pathOrUrl) {
int lastDotIndex = pathOrUrl.lastIndexOf('.');
if(lastDotIndex == -1) return pathOrUrl;
return pathOrUrl.substring(0, lastDotIndex);
}

/**
* Ensure that a directory exists, is a directory and is writable.
* @param targetFile the directory to check
Expand Down
1 change: 1 addition & 0 deletions app_pojavlauncher/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -429,4 +429,5 @@
<string name="bta_installer_available_versions">Supported BTA versions</string>
<string name="bta_installer_untested_versions">Untested BTA versions</string>
<string name="bta_installer_nightly_versions">Nightly BTA versions</string>
<string name="newdl_extracting_native_libraries">Extracting native libraries (%d/%d)</string>
</resources>
Loading