feat: add gradle export templates, support android app/lib and simple java

This commit is contained in:
Skylot
2025-04-29 21:07:52 +01:00
parent 9981949a2b
commit e3aa49aaa9
54 changed files with 1153 additions and 570 deletions
+19 -4
View File
@@ -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() {
@@ -38,5 +38,5 @@ android {
}
dependencies {
// some dependencies
// TODO: dependencies
}
@@ -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);