feat: add gradle export templates, support android app/lib and simple java
This commit is contained in:
@@ -39,6 +39,7 @@ import jadx.api.usage.impl.InMemoryUsageInfoCache;
|
||||
import jadx.core.deobf.DeobfAliasProvider;
|
||||
import jadx.core.deobf.conditions.DeobfWhitelist;
|
||||
import jadx.core.deobf.conditions.JadxRenameConditions;
|
||||
import jadx.core.export.ExportGradleType;
|
||||
import jadx.core.plugins.PluginContext;
|
||||
import jadx.core.plugins.files.IJadxFilesGetter;
|
||||
import jadx.core.plugins.files.TempFilesGetter;
|
||||
@@ -133,7 +134,7 @@ public class JadxArgs implements Closeable {
|
||||
private boolean escapeUnicode = false;
|
||||
private boolean replaceConsts = true;
|
||||
private boolean respectBytecodeAccModifiers = false;
|
||||
private boolean exportAsGradleProject = false;
|
||||
private @Nullable ExportGradleType exportGradleType = null;
|
||||
|
||||
private boolean restoreSwitchOverString = true;
|
||||
|
||||
@@ -567,11 +568,25 @@ public class JadxArgs implements Closeable {
|
||||
}
|
||||
|
||||
public boolean isExportAsGradleProject() {
|
||||
return exportAsGradleProject;
|
||||
return exportGradleType != null;
|
||||
}
|
||||
|
||||
public void setExportAsGradleProject(boolean exportAsGradleProject) {
|
||||
this.exportAsGradleProject = exportAsGradleProject;
|
||||
if (exportAsGradleProject) {
|
||||
if (exportGradleType == null) {
|
||||
exportGradleType = ExportGradleType.AUTO;
|
||||
}
|
||||
} else {
|
||||
exportGradleType = null;
|
||||
}
|
||||
}
|
||||
|
||||
public @Nullable ExportGradleType getExportGradleType() {
|
||||
return exportGradleType;
|
||||
}
|
||||
|
||||
public void setExportGradleType(@Nullable ExportGradleType exportGradleType) {
|
||||
this.exportGradleType = exportGradleType;
|
||||
}
|
||||
|
||||
public boolean isRestoreSwitchOverString() {
|
||||
@@ -861,7 +876,7 @@ public class JadxArgs implements Closeable {
|
||||
+ ", replaceConsts=" + replaceConsts
|
||||
+ ", restoreSwitchOverString=" + restoreSwitchOverString
|
||||
+ ", respectBytecodeAccModifiers=" + respectBytecodeAccModifiers
|
||||
+ ", exportAsGradleProject=" + exportAsGradleProject
|
||||
+ ", exportGradleType=" + exportGradleType
|
||||
+ ", skipXmlPrettyPrint=" + skipXmlPrettyPrint
|
||||
+ ", fsCaseSensitive=" + fsCaseSensitive
|
||||
+ ", renameFlags=" + renameFlags
|
||||
|
||||
@@ -7,6 +7,7 @@ import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
@@ -42,7 +43,8 @@ import jadx.core.dex.nodes.MethodNode;
|
||||
import jadx.core.dex.nodes.PackageNode;
|
||||
import jadx.core.dex.nodes.RootNode;
|
||||
import jadx.core.dex.visitors.SaveCode;
|
||||
import jadx.core.export.ExportGradleTask;
|
||||
import jadx.core.export.ExportGradle;
|
||||
import jadx.core.export.OutDirs;
|
||||
import jadx.core.plugins.JadxPluginManager;
|
||||
import jadx.core.plugins.PluginContext;
|
||||
import jadx.core.plugins.events.JadxEventsImpl;
|
||||
@@ -297,31 +299,28 @@ public final class JadxDecompiler implements Closeable {
|
||||
if (root == null) {
|
||||
throw new JadxRuntimeException("No loaded files");
|
||||
}
|
||||
File sourcesOutDir;
|
||||
File resOutDir;
|
||||
ExportGradleTask gradleExportTask;
|
||||
if (args.isExportAsGradleProject()) {
|
||||
gradleExportTask = new ExportGradleTask(resources, root, args.getOutDir());
|
||||
gradleExportTask.init();
|
||||
sourcesOutDir = gradleExportTask.getSrcOutDir();
|
||||
resOutDir = gradleExportTask.getResOutDir();
|
||||
OutDirs outDirs;
|
||||
ExportGradle gradleExport;
|
||||
if (args.getExportGradleType() != null) {
|
||||
gradleExport = new ExportGradle(root, args.getOutDir(), getResources());
|
||||
outDirs = gradleExport.init();
|
||||
} else {
|
||||
sourcesOutDir = args.getOutDirSrc();
|
||||
resOutDir = args.getOutDirRes();
|
||||
gradleExportTask = null;
|
||||
gradleExport = null;
|
||||
outDirs = new OutDirs(args.getOutDirSrc(), args.getOutDirRes());
|
||||
outDirs.makeDirs();
|
||||
}
|
||||
|
||||
TaskExecutor executor = new TaskExecutor();
|
||||
executor.setThreadsCount(args.getThreadsCount());
|
||||
if (saveResources) {
|
||||
// save resources first because decompilation can stop or fail
|
||||
appendResourcesSaveTasks(executor, resOutDir);
|
||||
appendResourcesSaveTasks(executor, outDirs.getResOutDir());
|
||||
}
|
||||
if (saveSources) {
|
||||
appendSourcesSave(executor, sourcesOutDir);
|
||||
appendSourcesSave(executor, outDirs.getSrcOutDir());
|
||||
}
|
||||
if (gradleExportTask != null) {
|
||||
executor.addSequentialTask(gradleExportTask);
|
||||
if (gradleExport != null) {
|
||||
executor.addSequentialTask(gradleExport::generateGradleFiles);
|
||||
}
|
||||
return executor;
|
||||
}
|
||||
@@ -340,6 +339,8 @@ public final class JadxDecompiler implements Closeable {
|
||||
Set<String> inputFileNames = args.getInputFiles().stream()
|
||||
.map(File::getAbsolutePath)
|
||||
.collect(Collectors.toSet());
|
||||
Set<String> codeSources = collectCodeSources();
|
||||
|
||||
List<Runnable> tasks = new ArrayList<>();
|
||||
for (ResourceFile resourceFile : getResources()) {
|
||||
ResourceType resType = resourceFile.getType();
|
||||
@@ -347,9 +348,14 @@ public final class JadxDecompiler implements Closeable {
|
||||
// already processed
|
||||
continue;
|
||||
}
|
||||
if (resType != ResourceType.ARSC
|
||||
&& inputFileNames.contains(resourceFile.getOriginalName())) {
|
||||
// ignore resource made from input file
|
||||
String resOriginalName = resourceFile.getOriginalName();
|
||||
if (resType != ResourceType.ARSC && inputFileNames.contains(resOriginalName)) {
|
||||
// ignore resource made from an input file
|
||||
continue;
|
||||
}
|
||||
if (codeSources.contains(resOriginalName)) {
|
||||
// don't output code source resources (.dex, .class, etc)
|
||||
// do not trust file extensions, use only sources set as class inputs
|
||||
continue;
|
||||
}
|
||||
tasks.add(new ResourcesSaver(this, outDir, resourceFile));
|
||||
@@ -357,6 +363,29 @@ public final class JadxDecompiler implements Closeable {
|
||||
executor.addParallelTasks(tasks);
|
||||
}
|
||||
|
||||
private Set<String> collectCodeSources() {
|
||||
Set<String> set = new HashSet<>();
|
||||
for (ClassNode cls : root.getClasses(true)) {
|
||||
if (cls.getClsData() == null) {
|
||||
// exclude synthetic classes
|
||||
continue;
|
||||
}
|
||||
String inputFileName = cls.getInputFileName();
|
||||
if (inputFileName.endsWith(".class")) {
|
||||
// cut .class name to get source .jar file
|
||||
// current template: "<optional input files>:<.jar>:<full class name>"
|
||||
// TODO: add property to set file name or reference to resource name
|
||||
int endIdx = inputFileName.lastIndexOf(':');
|
||||
if (endIdx != -1) {
|
||||
int startIdx = inputFileName.lastIndexOf(':', endIdx - 1) + 1;
|
||||
inputFileName = inputFileName.substring(startIdx, endIdx);
|
||||
}
|
||||
}
|
||||
set.add(inputFileName);
|
||||
}
|
||||
return set;
|
||||
}
|
||||
|
||||
private void appendSourcesSave(ITaskExecutor executor, File outDir) {
|
||||
List<JavaClass> classes = getClasses();
|
||||
List<JavaClass> processQueue = filterClasses(classes);
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
package jadx.api;
|
||||
|
||||
import jadx.core.xmlgen.ResContainer;
|
||||
|
||||
public class ResourceFileContainer extends ResourceFile {
|
||||
private final ResContainer container;
|
||||
|
||||
public ResourceFileContainer(String name, ResourceType type, ResContainer container) {
|
||||
super(null, name, type);
|
||||
this.container = container;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResContainer loadContent() {
|
||||
return container;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
package jadx.core.export;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.List;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import jadx.api.ResourceFile;
|
||||
import jadx.api.ResourceType;
|
||||
import jadx.core.dex.nodes.RootNode;
|
||||
import jadx.core.export.gen.AndroidGradleGenerator;
|
||||
import jadx.core.export.gen.IExportGradleGenerator;
|
||||
import jadx.core.export.gen.SimpleJavaGradleGenerator;
|
||||
import jadx.core.utils.android.AndroidManifestParser;
|
||||
import jadx.core.utils.exceptions.JadxRuntimeException;
|
||||
|
||||
public class ExportGradle {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(ExportGradle.class);
|
||||
private final RootNode root;
|
||||
private final File projectDir;
|
||||
private final List<ResourceFile> resources;
|
||||
private IExportGradleGenerator generator;
|
||||
|
||||
public ExportGradle(RootNode root, File projectDir, List<ResourceFile> resources) {
|
||||
this.root = root;
|
||||
this.projectDir = projectDir;
|
||||
this.resources = resources;
|
||||
}
|
||||
|
||||
public OutDirs init() {
|
||||
ExportGradleType exportType = getExportGradleType();
|
||||
LOG.info("Export Gradle project using '{}' template", exportType);
|
||||
switch (exportType) {
|
||||
case ANDROID_APP:
|
||||
case ANDROID_LIBRARY:
|
||||
generator = new AndroidGradleGenerator(root, projectDir, resources, exportType);
|
||||
break;
|
||||
case SIMPLE_JAVA:
|
||||
generator = new SimpleJavaGradleGenerator(root, projectDir, resources);
|
||||
break;
|
||||
default:
|
||||
throw new JadxRuntimeException("Unexpected export type: " + exportType);
|
||||
}
|
||||
generator.init();
|
||||
OutDirs outDirs = generator.getOutDirs();
|
||||
outDirs.makeDirs();
|
||||
return outDirs;
|
||||
}
|
||||
|
||||
private ExportGradleType getExportGradleType() {
|
||||
ExportGradleType argsExportType = root.getArgs().getExportGradleType();
|
||||
ExportGradleType detectedType = detectExportType(root, resources);
|
||||
if (argsExportType == null
|
||||
|| argsExportType == ExportGradleType.AUTO
|
||||
|| argsExportType == detectedType) {
|
||||
return detectedType;
|
||||
}
|
||||
return argsExportType;
|
||||
}
|
||||
|
||||
public static ExportGradleType detectExportType(RootNode root, List<ResourceFile> resources) {
|
||||
ResourceFile androidManifest = AndroidManifestParser.getAndroidManifest(resources);
|
||||
if (androidManifest != null) {
|
||||
if (resources.stream().anyMatch(r -> r.getOriginalName().equals("classes.jar"))) {
|
||||
return ExportGradleType.ANDROID_LIBRARY;
|
||||
}
|
||||
if (resources.stream().anyMatch(r -> r.getType() == ResourceType.ARSC)) {
|
||||
return ExportGradleType.ANDROID_APP;
|
||||
}
|
||||
}
|
||||
return ExportGradleType.SIMPLE_JAVA;
|
||||
}
|
||||
|
||||
public void generateGradleFiles() {
|
||||
generator.generateFiles();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,119 +0,0 @@
|
||||
package jadx.core.export;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.EnumSet;
|
||||
import java.util.List;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import jadx.api.ResourceFile;
|
||||
import jadx.core.dex.nodes.RootNode;
|
||||
import jadx.core.utils.android.AndroidManifestParser;
|
||||
import jadx.core.utils.android.AppAttribute;
|
||||
import jadx.core.utils.android.ApplicationParams;
|
||||
import jadx.core.utils.exceptions.JadxRuntimeException;
|
||||
import jadx.core.xmlgen.ResContainer;
|
||||
|
||||
public class ExportGradleProject {
|
||||
private static final Pattern ILLEGAL_GRADLE_CHARS = Pattern.compile("[/\\\\:>\"?*|]");
|
||||
|
||||
private final RootNode root;
|
||||
private final File projectDir;
|
||||
private final File appDir;
|
||||
private final ApplicationParams applicationParams;
|
||||
|
||||
public ExportGradleProject(RootNode root, File projectDir, ResourceFile androidManifest, ResContainer appStrings) {
|
||||
this.root = root;
|
||||
this.projectDir = projectDir;
|
||||
this.appDir = new File(projectDir, "app");
|
||||
this.applicationParams = getApplicationParams(androidManifest, appStrings);
|
||||
}
|
||||
|
||||
public void generateGradleFiles() {
|
||||
try {
|
||||
saveProjectBuildGradle();
|
||||
saveApplicationBuildGradle();
|
||||
saveSettingsGradle();
|
||||
saveGradleProperties();
|
||||
} catch (Exception e) {
|
||||
throw new JadxRuntimeException("Gradle export failed", e);
|
||||
}
|
||||
}
|
||||
|
||||
private void saveGradleProperties() throws IOException {
|
||||
GradleInfoStorage gradleInfo = root.getGradleInfoStorage();
|
||||
/*
|
||||
* For Android Gradle Plugin >=8.0.0 the property "android.nonFinalResIds=false" has to be set in
|
||||
* "gradle.properties" when resource identifiers are used as constant expressions.
|
||||
*/
|
||||
if (gradleInfo.isNonFinalResIds()) {
|
||||
File gradlePropertiesFile = new File(projectDir, "gradle.properties");
|
||||
try (FileOutputStream fos = new FileOutputStream(gradlePropertiesFile)) {
|
||||
fos.write("android.nonFinalResIds=false".getBytes(StandardCharsets.UTF_8));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void saveProjectBuildGradle() throws IOException {
|
||||
TemplateFile tmpl = TemplateFile.fromResources("/export/build.gradle.tmpl");
|
||||
tmpl.save(new File(projectDir, "build.gradle"));
|
||||
}
|
||||
|
||||
private void saveSettingsGradle() throws IOException {
|
||||
TemplateFile tmpl = TemplateFile.fromResources("/export/settings.gradle.tmpl");
|
||||
|
||||
tmpl.add("applicationName", ILLEGAL_GRADLE_CHARS.matcher(applicationParams.getApplicationName()).replaceAll(""));
|
||||
tmpl.save(new File(projectDir, "settings.gradle"));
|
||||
}
|
||||
|
||||
private void saveApplicationBuildGradle() throws IOException {
|
||||
TemplateFile tmpl = TemplateFile.fromResources("/export/app.build.gradle.tmpl");
|
||||
String appPackage = root.getAppPackage();
|
||||
|
||||
if (appPackage == null) {
|
||||
appPackage = "UNKNOWN";
|
||||
}
|
||||
|
||||
Integer minSdkVersion = applicationParams.getMinSdkVersion();
|
||||
|
||||
tmpl.add("applicationId", appPackage);
|
||||
tmpl.add("minSdkVersion", minSdkVersion);
|
||||
tmpl.add("targetSdkVersion", applicationParams.getTargetSdkVersion());
|
||||
tmpl.add("versionCode", applicationParams.getVersionCode());
|
||||
tmpl.add("versionName", applicationParams.getVersionName());
|
||||
|
||||
List<String> additionalOptions = new ArrayList<>();
|
||||
GradleInfoStorage gradleInfo = root.getGradleInfoStorage();
|
||||
if (gradleInfo.isVectorPathData() && minSdkVersion < 21 || gradleInfo.isVectorFillType() && minSdkVersion < 24) {
|
||||
additionalOptions.add("vectorDrawables.useSupportLibrary = true");
|
||||
}
|
||||
if (gradleInfo.isUseApacheHttpLegacy()) {
|
||||
additionalOptions.add("useLibrary 'org.apache.http.legacy'");
|
||||
}
|
||||
genAdditionalAndroidPluginOptions(tmpl, additionalOptions);
|
||||
|
||||
tmpl.save(new File(appDir, "build.gradle"));
|
||||
}
|
||||
|
||||
private void genAdditionalAndroidPluginOptions(TemplateFile tmpl, List<String> additionalOptions) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (String additionalOption : additionalOptions) {
|
||||
sb.append(" ").append(additionalOption).append('\n');
|
||||
}
|
||||
tmpl.add("additionalOptions", sb.toString());
|
||||
}
|
||||
|
||||
private ApplicationParams getApplicationParams(ResourceFile androidManifest, ResContainer appStrings) {
|
||||
AndroidManifestParser parser = new AndroidManifestParser(androidManifest, appStrings, EnumSet.of(
|
||||
AppAttribute.APPLICATION_LABEL,
|
||||
AppAttribute.MIN_SDK_VERSION,
|
||||
AppAttribute.TARGET_SDK_VERSION,
|
||||
AppAttribute.VERSION_CODE,
|
||||
AppAttribute.VERSION_NAME),
|
||||
root.getArgs().getSecurity());
|
||||
return parser.parse();
|
||||
}
|
||||
}
|
||||
@@ -1,70 +0,0 @@
|
||||
package jadx.core.export;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.List;
|
||||
|
||||
import jadx.api.ResourceFile;
|
||||
import jadx.api.ResourceType;
|
||||
import jadx.core.dex.nodes.RootNode;
|
||||
import jadx.core.utils.android.AndroidManifestParser;
|
||||
import jadx.core.utils.files.FileUtils;
|
||||
import jadx.core.xmlgen.ResContainer;
|
||||
|
||||
public class ExportGradleTask implements Runnable {
|
||||
|
||||
private final List<ResourceFile> resources;
|
||||
|
||||
private final RootNode root;
|
||||
private final File projectDir;
|
||||
private final File srcOutDir;
|
||||
private final File resOutDir;
|
||||
|
||||
public ExportGradleTask(List<ResourceFile> resources, RootNode root, File projectDir) {
|
||||
this.resources = resources;
|
||||
this.projectDir = projectDir;
|
||||
this.root = root;
|
||||
File appDir = new File(projectDir, "app");
|
||||
this.srcOutDir = new File(appDir, "src/main/java");
|
||||
this.resOutDir = new File(appDir, "src/main");
|
||||
}
|
||||
|
||||
public void init() {
|
||||
FileUtils.makeDirs(srcOutDir);
|
||||
FileUtils.makeDirs(resOutDir);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
ResourceFile androidManifest = AndroidManifestParser.getAndroidManifest(resources);
|
||||
if (androidManifest == null) {
|
||||
throw new IllegalStateException("Could not find AndroidManifest.xml");
|
||||
}
|
||||
|
||||
List<ResContainer> resContainers = resources.stream()
|
||||
.filter(resourceFile -> resourceFile.getType() == ResourceType.ARSC)
|
||||
.findFirst()
|
||||
.orElseThrow(IllegalStateException::new)
|
||||
.loadContent()
|
||||
.getSubFiles();
|
||||
|
||||
ResContainer strings = resContainers
|
||||
.stream()
|
||||
.filter(resContainer -> resContainer.getName().contains("values/strings.xml"))
|
||||
.findFirst()
|
||||
.orElseGet(() -> resContainers.stream()
|
||||
.filter(resContainer -> resContainer.getFileName().contains("strings.xml"))
|
||||
.findFirst()
|
||||
.orElse(null));
|
||||
|
||||
ExportGradleProject export = new ExportGradleProject(root, projectDir, androidManifest, strings);
|
||||
export.generateGradleFiles();
|
||||
}
|
||||
|
||||
public File getSrcOutDir() {
|
||||
return srcOutDir;
|
||||
}
|
||||
|
||||
public File getResOutDir() {
|
||||
return resOutDir;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package jadx.core.export;
|
||||
|
||||
public enum ExportGradleType {
|
||||
AUTO("Auto"),
|
||||
ANDROID_APP("Android App"),
|
||||
ANDROID_LIBRARY("Android Library"),
|
||||
SIMPLE_JAVA("Simple Java");
|
||||
|
||||
private final String desc;
|
||||
|
||||
ExportGradleType(String desc) {
|
||||
this.desc = desc;
|
||||
}
|
||||
|
||||
public String getDesc() {
|
||||
return desc;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return desc;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package jadx.core.export;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
import jadx.core.utils.files.FileUtils;
|
||||
|
||||
public class OutDirs {
|
||||
private final File srcOutDir;
|
||||
private final File resOutDir;
|
||||
|
||||
public OutDirs(File srcOutDir, File resOutDir) {
|
||||
this.srcOutDir = srcOutDir;
|
||||
this.resOutDir = resOutDir;
|
||||
}
|
||||
|
||||
public File getSrcOutDir() {
|
||||
return srcOutDir;
|
||||
}
|
||||
|
||||
public File getResOutDir() {
|
||||
return resOutDir;
|
||||
}
|
||||
|
||||
public void makeDirs() {
|
||||
FileUtils.makeDirs(srcOutDir);
|
||||
FileUtils.makeDirs(resOutDir);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,197 @@
|
||||
package jadx.core.export.gen;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.EnumSet;
|
||||
import java.util.List;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import jadx.api.ResourceFile;
|
||||
import jadx.api.ResourceType;
|
||||
import jadx.api.security.IJadxSecurity;
|
||||
import jadx.core.dex.nodes.RootNode;
|
||||
import jadx.core.export.ExportGradleType;
|
||||
import jadx.core.export.GradleInfoStorage;
|
||||
import jadx.core.export.OutDirs;
|
||||
import jadx.core.export.TemplateFile;
|
||||
import jadx.core.utils.Utils;
|
||||
import jadx.core.utils.android.AndroidManifestParser;
|
||||
import jadx.core.utils.android.AppAttribute;
|
||||
import jadx.core.utils.android.ApplicationParams;
|
||||
import jadx.core.utils.exceptions.JadxRuntimeException;
|
||||
import jadx.core.xmlgen.ResContainer;
|
||||
|
||||
public class AndroidGradleGenerator implements IExportGradleGenerator {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(AndroidGradleGenerator.class);
|
||||
private static final Pattern ILLEGAL_GRADLE_CHARS = Pattern.compile("[/\\\\:>\"?*|]");
|
||||
|
||||
private static final ApplicationParams UNKNOWN_APP_PARAMS = new ApplicationParams("UNKNOWN", 0, 0, 0, "UNKNOWN", "UNKNOWN", "UNKNOWN");
|
||||
|
||||
private final RootNode root;
|
||||
private final File projectDir;
|
||||
private final List<ResourceFile> resources;
|
||||
private final boolean exportApp;
|
||||
|
||||
private OutDirs outDirs;
|
||||
private File baseDir;
|
||||
private ApplicationParams applicationParams;
|
||||
|
||||
public AndroidGradleGenerator(RootNode root, File projectDir, List<ResourceFile> resources, ExportGradleType exportType) {
|
||||
this.root = root;
|
||||
this.projectDir = projectDir;
|
||||
this.resources = resources;
|
||||
this.exportApp = exportType == ExportGradleType.ANDROID_APP;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init() {
|
||||
String moduleDir = exportApp ? "app" : "lib";
|
||||
baseDir = new File(projectDir, moduleDir);
|
||||
outDirs = new OutDirs(new File(baseDir, "src/main/java"), new File(baseDir, "src/main"));
|
||||
applicationParams = parseApplicationParams();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void generateFiles() {
|
||||
try {
|
||||
saveProjectBuildGradle();
|
||||
if (exportApp) {
|
||||
saveApplicationBuildGradle();
|
||||
} else {
|
||||
saveLibraryBuildGradle();
|
||||
}
|
||||
saveSettingsGradle();
|
||||
saveGradleProperties();
|
||||
} catch (Exception e) {
|
||||
throw new JadxRuntimeException("Gradle export failed", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public OutDirs getOutDirs() {
|
||||
return outDirs;
|
||||
}
|
||||
|
||||
private ApplicationParams parseApplicationParams() {
|
||||
try {
|
||||
ResourceFile androidManifest = AndroidManifestParser.getAndroidManifest(resources);
|
||||
if (androidManifest == null) {
|
||||
LOG.warn("AndroidManifest.xml not found, exported files will contains 'UNKNOWN' fields");
|
||||
return UNKNOWN_APP_PARAMS;
|
||||
}
|
||||
ResContainer strings = null;
|
||||
if (exportApp) {
|
||||
ResourceFile arscFile = resources.stream()
|
||||
.filter(resourceFile -> resourceFile.getType() == ResourceType.ARSC)
|
||||
.findFirst().orElse(null);
|
||||
if (arscFile != null) {
|
||||
List<ResContainer> resContainers = arscFile.loadContent().getSubFiles();
|
||||
strings = resContainers
|
||||
.stream()
|
||||
.filter(resContainer -> resContainer.getName().contains("values/strings.xml"))
|
||||
.findFirst()
|
||||
.orElseGet(() -> resContainers.stream()
|
||||
.filter(resContainer -> resContainer.getName().contains("strings.xml"))
|
||||
.findFirst().orElse(null));
|
||||
}
|
||||
}
|
||||
|
||||
EnumSet<AppAttribute> attrs = EnumSet.noneOf(AppAttribute.class);
|
||||
attrs.add(AppAttribute.MIN_SDK_VERSION);
|
||||
if (exportApp) {
|
||||
attrs.add(AppAttribute.APPLICATION_LABEL);
|
||||
attrs.add(AppAttribute.TARGET_SDK_VERSION);
|
||||
attrs.add(AppAttribute.VERSION_NAME);
|
||||
attrs.add(AppAttribute.VERSION_CODE);
|
||||
}
|
||||
|
||||
IJadxSecurity security = root.getArgs().getSecurity();
|
||||
AndroidManifestParser parser = new AndroidManifestParser(androidManifest, strings, attrs, security);
|
||||
return parser.parse();
|
||||
} catch (Throwable t) {
|
||||
LOG.warn("Failed to parse AndroidManifest.xml", t);
|
||||
return UNKNOWN_APP_PARAMS;
|
||||
}
|
||||
}
|
||||
|
||||
private void saveGradleProperties() throws IOException {
|
||||
GradleInfoStorage gradleInfo = root.getGradleInfoStorage();
|
||||
/*
|
||||
* For Android Gradle Plugin >=8.0.0 the property "android.nonFinalResIds=false" has to be set in
|
||||
* "gradle.properties" when resource identifiers are used as constant expressions.
|
||||
*/
|
||||
if (gradleInfo.isNonFinalResIds()) {
|
||||
File gradlePropertiesFile = new File(projectDir, "gradle.properties");
|
||||
try (FileOutputStream fos = new FileOutputStream(gradlePropertiesFile)) {
|
||||
fos.write("android.nonFinalResIds=false".getBytes(StandardCharsets.UTF_8));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void saveProjectBuildGradle() throws IOException {
|
||||
TemplateFile tmpl = TemplateFile.fromResources("/export/android/build.gradle.tmpl");
|
||||
tmpl.save(new File(projectDir, "build.gradle"));
|
||||
}
|
||||
|
||||
private void saveSettingsGradle() throws IOException {
|
||||
TemplateFile tmpl = TemplateFile.fromResources("/export/android/settings.gradle.tmpl");
|
||||
String appName = applicationParams.getApplicationName();
|
||||
String projectName;
|
||||
if (appName != null) {
|
||||
projectName = ILLEGAL_GRADLE_CHARS.matcher(appName).replaceAll("");
|
||||
} else {
|
||||
projectName = GradleGeneratorTools.guessProjectName(root);
|
||||
}
|
||||
tmpl.add("projectName", projectName);
|
||||
tmpl.add("mainModuleName", baseDir.getName());
|
||||
tmpl.save(new File(projectDir, "settings.gradle"));
|
||||
}
|
||||
|
||||
private void saveApplicationBuildGradle() throws IOException {
|
||||
String appPackage = Utils.getOrElse(root.getAppPackage(), "UNKNOWN");
|
||||
int minSdkVersion = Utils.getOrElse(applicationParams.getMinSdkVersion(), 0);
|
||||
|
||||
TemplateFile tmpl = TemplateFile.fromResources("/export/android/app.build.gradle.tmpl");
|
||||
tmpl.add("applicationId", appPackage);
|
||||
tmpl.add("minSdkVersion", minSdkVersion);
|
||||
tmpl.add("targetSdkVersion", applicationParams.getTargetSdkVersion());
|
||||
tmpl.add("versionCode", applicationParams.getVersionCode());
|
||||
tmpl.add("versionName", applicationParams.getVersionName());
|
||||
tmpl.add("additionalOptions", genAdditionalAndroidPluginOptions(minSdkVersion));
|
||||
tmpl.save(new File(baseDir, "build.gradle"));
|
||||
}
|
||||
|
||||
private void saveLibraryBuildGradle() throws IOException {
|
||||
String pkg = Utils.getOrElse(root.getAppPackage(), "UNKNOWN");
|
||||
int minSdkVersion = Utils.getOrElse(applicationParams.getMinSdkVersion(), 0);
|
||||
|
||||
TemplateFile tmpl = TemplateFile.fromResources("/export/android/lib.build.gradle.tmpl");
|
||||
tmpl.add("packageId", pkg);
|
||||
tmpl.add("minSdkVersion", minSdkVersion);
|
||||
tmpl.add("additionalOptions", genAdditionalAndroidPluginOptions(minSdkVersion));
|
||||
|
||||
tmpl.save(new File(baseDir, "build.gradle"));
|
||||
}
|
||||
|
||||
private String genAdditionalAndroidPluginOptions(int minSdkVersion) {
|
||||
List<String> additionalOptions = new ArrayList<>();
|
||||
GradleInfoStorage gradleInfo = root.getGradleInfoStorage();
|
||||
if (gradleInfo.isVectorPathData() && minSdkVersion < 21 || gradleInfo.isVectorFillType() && minSdkVersion < 24) {
|
||||
additionalOptions.add("vectorDrawables.useSupportLibrary = true");
|
||||
}
|
||||
if (gradleInfo.isUseApacheHttpLegacy()) {
|
||||
additionalOptions.add("useLibrary 'org.apache.http.legacy'");
|
||||
}
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (String additionalOption : additionalOptions) {
|
||||
sb.append(" ").append(additionalOption).append('\n');
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package jadx.core.export.gen;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.List;
|
||||
|
||||
import jadx.core.dex.nodes.RootNode;
|
||||
import jadx.core.utils.files.FileUtils;
|
||||
|
||||
public class GradleGeneratorTools {
|
||||
|
||||
public static String guessProjectName(RootNode root) {
|
||||
List<File> inputFiles = root.getArgs().getInputFiles();
|
||||
if (inputFiles.size() == 1) {
|
||||
return FileUtils.getPathBaseName(inputFiles.get(0).toPath());
|
||||
}
|
||||
// default
|
||||
return "PROJECT_NAME";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package jadx.core.export.gen;
|
||||
|
||||
import jadx.core.export.OutDirs;
|
||||
|
||||
public interface IExportGradleGenerator {
|
||||
|
||||
void init();
|
||||
|
||||
OutDirs getOutDirs();
|
||||
|
||||
void generateFiles();
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
package jadx.core.export.gen;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
|
||||
import jadx.api.ResourceFile;
|
||||
import jadx.core.dex.nodes.RootNode;
|
||||
import jadx.core.export.OutDirs;
|
||||
import jadx.core.export.TemplateFile;
|
||||
import jadx.core.utils.exceptions.JadxRuntimeException;
|
||||
|
||||
public class SimpleJavaGradleGenerator implements IExportGradleGenerator {
|
||||
private final RootNode root;
|
||||
private final File projectDir;
|
||||
private final List<ResourceFile> resources;
|
||||
|
||||
private OutDirs outDirs;
|
||||
private File appDir;
|
||||
|
||||
public SimpleJavaGradleGenerator(RootNode root, File projectDir, List<ResourceFile> resources) {
|
||||
this.root = root;
|
||||
this.projectDir = projectDir;
|
||||
this.resources = resources;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init() {
|
||||
appDir = new File(projectDir, "app");
|
||||
File srcOutDir = new File(appDir, "src/main/java");
|
||||
File resOutDir = new File(appDir, "src/main/resources");
|
||||
outDirs = new OutDirs(srcOutDir, resOutDir);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void generateFiles() {
|
||||
try {
|
||||
saveSettingsGradle();
|
||||
saveBuildGradle();
|
||||
} catch (Exception e) {
|
||||
throw new JadxRuntimeException("Failed to generate gradle files", e);
|
||||
}
|
||||
}
|
||||
|
||||
private void saveSettingsGradle() throws IOException {
|
||||
TemplateFile tmpl = TemplateFile.fromResources("/export/java/settings.gradle.kts.tmpl");
|
||||
tmpl.add("projectName", GradleGeneratorTools.guessProjectName(root));
|
||||
tmpl.save(new File(projectDir, "settings.gradle.kts"));
|
||||
}
|
||||
|
||||
private void saveBuildGradle() throws IOException {
|
||||
TemplateFile tmpl = TemplateFile.fromResources("/export/java/build.gradle.kts.tmpl");
|
||||
tmpl.save(new File(appDir, "build.gradle.kts"));
|
||||
}
|
||||
|
||||
@Override
|
||||
public OutDirs getOutDirs() {
|
||||
return outDirs;
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import java.nio.file.Path;
|
||||
|
||||
import jadx.api.plugins.JadxPluginInfo;
|
||||
import jadx.api.plugins.data.IJadxFiles;
|
||||
import jadx.core.utils.files.FileUtils;
|
||||
|
||||
public class JadxFilesData implements IJadxFiles {
|
||||
private static final String PLUGINS_DATA_DIR = "plugins-data";
|
||||
@@ -32,6 +33,8 @@ public class JadxFilesData implements IJadxFiles {
|
||||
}
|
||||
|
||||
private Path toPluginPath(Path dir) {
|
||||
return dir.resolve(PLUGINS_DATA_DIR).resolve(pluginInfo.getPluginId());
|
||||
Path dirPath = dir.resolve(PLUGINS_DATA_DIR).resolve(pluginInfo.getPluginId());
|
||||
FileUtils.makeDirs(dirPath);
|
||||
return dirPath;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ import jadx.core.xmlgen.ResContainer;
|
||||
|
||||
public class AndroidManifestParser {
|
||||
private final Document androidManifest;
|
||||
private final Document appStrings;
|
||||
private final @Nullable Document appStrings;
|
||||
private final EnumSet<AppAttribute> parseAttrs;
|
||||
private final IJadxSecurity security;
|
||||
|
||||
@@ -28,7 +28,7 @@ public class AndroidManifestParser {
|
||||
this(androidManifestRes, null, parseAttrs, security);
|
||||
}
|
||||
|
||||
public AndroidManifestParser(ResourceFile androidManifestRes, ResContainer appStrings,
|
||||
public AndroidManifestParser(ResourceFile androidManifestRes, @Nullable ResContainer appStrings,
|
||||
EnumSet<AppAttribute> parseAttrs, IJadxSecurity security) {
|
||||
this.parseAttrs = parseAttrs;
|
||||
this.security = Objects.requireNonNull(security);
|
||||
|
||||
@@ -10,17 +10,17 @@ public class ApplicationParams {
|
||||
private final Integer targetSdkVersion;
|
||||
private final Integer versionCode;
|
||||
private final String versionName;
|
||||
private final String mainActivtiy;
|
||||
private final String mainActivity;
|
||||
private final String application;
|
||||
|
||||
public ApplicationParams(String applicationLabel, Integer minSdkVersion, Integer targetSdkVersion, Integer versionCode,
|
||||
String versionName, String mainActivtiy, String application) {
|
||||
String versionName, String mainActivity, String application) {
|
||||
this.applicationLabel = applicationLabel;
|
||||
this.minSdkVersion = minSdkVersion;
|
||||
this.targetSdkVersion = targetSdkVersion;
|
||||
this.versionCode = versionCode;
|
||||
this.versionName = versionName;
|
||||
this.mainActivtiy = mainActivtiy;
|
||||
this.mainActivity = mainActivity;
|
||||
this.application = application;
|
||||
}
|
||||
|
||||
@@ -45,11 +45,11 @@ public class ApplicationParams {
|
||||
}
|
||||
|
||||
public String getMainActivity() {
|
||||
return mainActivtiy;
|
||||
return mainActivity;
|
||||
}
|
||||
|
||||
public JavaClass getMainActivityJavaClass(JadxDecompiler decompiler) {
|
||||
return decompiler.searchJavaClassByOrigFullName(mainActivtiy);
|
||||
return decompiler.searchJavaClassByOrigFullName(mainActivity);
|
||||
}
|
||||
|
||||
public String getApplication() {
|
||||
|
||||
+1
-1
@@ -38,5 +38,5 @@ android {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
// some dependencies
|
||||
// TODO: dependencies
|
||||
}
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
buildscript {
|
||||
repositories {
|
||||
google()
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
dependencies {
|
||||
@@ -0,0 +1,36 @@
|
||||
plugins {
|
||||
id 'com.android.library'
|
||||
}
|
||||
|
||||
android {
|
||||
namespace '{{packageId}}'
|
||||
compileSdk 30
|
||||
|
||||
defaultConfig {
|
||||
minSdk {{minSdkVersion}}
|
||||
|
||||
{{additionalOptions}}
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
minifyEnabled false
|
||||
}
|
||||
}
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
}
|
||||
|
||||
lintOptions {
|
||||
abortOnError false
|
||||
}
|
||||
|
||||
buildFeatures {
|
||||
buildConfig = false
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
// TODO: dependencies
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
rootProject.name = '{{projectName}}'
|
||||
|
||||
include '{{mainModuleName}}'
|
||||
@@ -0,0 +1,12 @@
|
||||
plugins {
|
||||
java
|
||||
}
|
||||
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
// some dependencies
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
rootProject.name = "{{projectName}}"
|
||||
|
||||
include("app")
|
||||
@@ -1,2 +0,0 @@
|
||||
include ':app'
|
||||
rootProject.name = '{{applicationName}}'
|
||||
@@ -2,6 +2,8 @@ package jadx.tests.api;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.util.List;
|
||||
|
||||
@@ -10,19 +12,21 @@ import org.junit.jupiter.api.io.TempDir;
|
||||
import jadx.api.ICodeInfo;
|
||||
import jadx.api.JadxArgs;
|
||||
import jadx.api.ResourceFile;
|
||||
import jadx.api.ResourceFileContainer;
|
||||
import jadx.api.ResourceFileContent;
|
||||
import jadx.api.ResourceType;
|
||||
import jadx.api.impl.SimpleCodeInfo;
|
||||
import jadx.core.dex.nodes.RootNode;
|
||||
import jadx.core.export.ExportGradleProject;
|
||||
import jadx.core.export.ExportGradleTask;
|
||||
import jadx.core.export.ExportGradle;
|
||||
import jadx.core.export.ExportGradleType;
|
||||
import jadx.core.export.OutDirs;
|
||||
import jadx.core.xmlgen.ResContainer;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.fail;
|
||||
|
||||
public abstract class ExportGradleTest {
|
||||
private static final String MANIFEST_TESTS_DIR = "src/test/manifest";
|
||||
private static final String MANIFEST_TESTS_DIR = "manifest";
|
||||
|
||||
private final RootNode root = new RootNode(new JadxArgs());
|
||||
|
||||
@@ -30,7 +34,7 @@ public abstract class ExportGradleTest {
|
||||
private File exportDir;
|
||||
|
||||
protected ICodeInfo loadResource(String filename) {
|
||||
return new SimpleCodeInfo(loadFileContent(new File(MANIFEST_TESTS_DIR, filename)));
|
||||
return new SimpleCodeInfo(loadResourceContent(MANIFEST_TESTS_DIR, filename));
|
||||
}
|
||||
|
||||
private static String loadFileContent(File filePath) {
|
||||
@@ -42,21 +46,37 @@ public abstract class ExportGradleTest {
|
||||
}
|
||||
}
|
||||
|
||||
private String loadResourceContent(String dir, String filename) {
|
||||
String resPath = dir + '/' + filename;
|
||||
try (InputStream in = getClass().getClassLoader().getResourceAsStream(resPath)) {
|
||||
if (in == null) {
|
||||
fail("Resource not found: " + resPath);
|
||||
return "";
|
||||
}
|
||||
return new String(in.readAllBytes(), StandardCharsets.UTF_8);
|
||||
} catch (Exception e) {
|
||||
fail("Loading file failed: " + resPath, e);
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
protected RootNode getRootNode() {
|
||||
return root;
|
||||
}
|
||||
|
||||
protected void exportGradle(String manifestFilename, String stringsFileName) {
|
||||
ResourceFile androidManifest = new ResourceFileContent(manifestFilename,
|
||||
ResourceType.XML, loadResource(manifestFilename));
|
||||
ResourceFile androidManifest =
|
||||
new ResourceFileContent("AndroidManifest.xml", ResourceType.MANIFEST, loadResource(manifestFilename));
|
||||
ResContainer strings = ResContainer.textResource(stringsFileName, loadResource(stringsFileName));
|
||||
ResContainer arsc = ResContainer.resourceTable("resources.arsc", List.of(strings), new SimpleCodeInfo("empty"));
|
||||
ResourceFile arscFile = new ResourceFileContainer("resources.arsc", ResourceType.ARSC, arsc);
|
||||
List<ResourceFile> resources = List.of(androidManifest, arscFile);
|
||||
|
||||
ExportGradleTask exportGradleTask = new ExportGradleTask(List.of(androidManifest), root, exportDir);
|
||||
exportGradleTask.init();
|
||||
assertThat(exportGradleTask.getSrcOutDir()).exists();
|
||||
assertThat(exportGradleTask.getResOutDir()).exists();
|
||||
|
||||
ExportGradleProject export = new ExportGradleProject(root, exportDir, androidManifest, strings);
|
||||
root.getArgs().setExportGradleType(ExportGradleType.ANDROID_APP);
|
||||
ExportGradle export = new ExportGradle(root, exportDir, resources);
|
||||
OutDirs outDirs = export.init();
|
||||
assertThat(outDirs.getSrcOutDir()).exists();
|
||||
assertThat(outDirs.getResOutDir()).exists();
|
||||
export.generateGradleFiles();
|
||||
}
|
||||
|
||||
@@ -72,7 +92,7 @@ public abstract class ExportGradleTest {
|
||||
return new File(exportDir, "gradle.properties");
|
||||
}
|
||||
|
||||
protected String getGradleProperies() {
|
||||
protected String getGradleProperties() {
|
||||
return loadFileContent(getGradleProperiesFile());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,6 @@ public class TestNonFinalResIds extends ExportGradleTest {
|
||||
|
||||
gradleInfo.setNonFinalResIds(true);
|
||||
exportGradle("OptionalTargetSdkVersion.xml", "strings.xml");
|
||||
assertThat(getGradleProperies()).containsOne("android.nonFinalResIds=false");
|
||||
assertThat(getGradleProperties()).containsOne("android.nonFinalResIds=false");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ public class TemplateFileTest {
|
||||
|
||||
@Test
|
||||
public void testBuildGradle() throws Exception {
|
||||
TemplateFile tmpl = TemplateFile.fromResources("/export/app.build.gradle.tmpl");
|
||||
TemplateFile tmpl = TemplateFile.fromResources("/export/android/app.build.gradle.tmpl");
|
||||
tmpl.add("applicationId", "SOME_ID");
|
||||
tmpl.add("minSdkVersion", 1);
|
||||
tmpl.add("targetSdkVersion", 2);
|
||||
|
||||
Reference in New Issue
Block a user