Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 1 addition & 6 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,6 @@ jobs:
- name: Cache Gradle
uses: burrunan/gradle-cache-action@v3

- name: Build
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: ./gradlew build clean

- name: Setup Node.js
uses: actions/setup-node@v6
with:
Expand All @@ -40,7 +35,7 @@ jobs:
run: npm install

- name: Import GPG key
uses: crazy-max/ghaction-import-gpg@v6
uses: crazy-max/ghaction-import-gpg@v7
with:
gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }}
passphrase: ${{ secrets.GPG_PASSPHRASE }}
Expand Down
42 changes: 42 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,45 @@
# [1.8.0-dev.6](https://github.com/MorpheApp/morphe-cli/compare/v1.8.0-dev.5...v1.8.0-dev.6) (2026-04-21)


### Bug Fixes

* continue-on-error fix + force windows to `FULL` ([#120](https://github.com/MorpheApp/morphe-cli/issues/120)) ([036faba](https://github.com/MorpheApp/morphe-cli/commit/036faba68f8f0c1f683f3c6222f08d0377217266))

# [1.8.0-dev.5](https://github.com/MorpheApp/morphe-cli/compare/v1.8.0-dev.4...v1.8.0-dev.5) (2026-04-20)


### Bug Fixes

* Re-enable strip libs feature ([#117](https://github.com/MorpheApp/morphe-cli/issues/117)) ([6d2bb94](https://github.com/MorpheApp/morphe-cli/commit/6d2bb94425e3fa040cc0d00261b77559d69e4470))

# [1.8.0-dev.4](https://github.com/MorpheApp/morphe-cli/compare/v1.8.0-dev.3...v1.8.0-dev.4) (2026-04-19)


### Bug Fixes

* Update to latest patcher ([efd0cf1](https://github.com/MorpheApp/morphe-cli/commit/efd0cf16dbac6625a54e2f82881bbe16c9e8acce))

# [1.8.0-dev.3](https://github.com/MorpheApp/morphe-cli/compare/v1.8.0-dev.2...v1.8.0-dev.3) (2026-04-18)


### Features

* Add in-app dependency license viewer ([#114](https://github.com/MorpheApp/morphe-cli/issues/114)) ([77a1dde](https://github.com/MorpheApp/morphe-cli/commit/77a1ddea595dd83331c9d0ce246620ebd64bcd22))

# [1.8.0-dev.2](https://github.com/MorpheApp/morphe-cli/compare/v1.8.0-dev.1...v1.8.0-dev.2) (2026-04-18)


### Bug Fixes

* Include Notice and License files in shadow jar release ([#113](https://github.com/MorpheApp/morphe-cli/issues/113)) ([a9a2402](https://github.com/MorpheApp/morphe-cli/commit/a9a2402209bd0ddd273c0aa5185888b74c69d118))

# [1.8.0-dev.1](https://github.com/MorpheApp/morphe-cli/compare/v1.7.0...v1.8.0-dev.1) (2026-04-18)


### Features

* Faster dex rebuilding + reduced memory requirements ([#108](https://github.com/MorpheApp/morphe-cli/issues/108)) ([ed8918e](https://github.com/MorpheApp/morphe-cli/commit/ed8918eb8d26634f717e09b58aa174c1d1353a07))

# [1.7.0](https://github.com/MorpheApp/morphe-cli/compare/v1.6.3...v1.7.0) (2026-04-16)


Expand Down
6 changes: 6 additions & 0 deletions aboutlibraries/libraries/app.morphe.cli.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"uniqueId": "app.morphe.cli",
"name": "Morphe CLI",
"developers": [{"name": "Morphe"}],
"licenses": ["GPL-3.0-only"]
}
66 changes: 65 additions & 1 deletion build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
import com.mikepenz.aboutlibraries.plugin.DuplicateMode
import com.mikepenz.aboutlibraries.plugin.DuplicateRule

plugins {
alias(libs.plugins.kotlin.jvm)
alias(libs.plugins.kotlin.compose)
alias(libs.plugins.kotlin.serialization)
alias(libs.plugins.compose)
alias(libs.plugins.shadow)
alias(libs.plugins.about.libraries)
// Shadow plugin is provided by buildSrc to enable the custom NoticeMergeTransformer.
// Applied without a version here; the version is pinned in buildSrc/build.gradle.kts.
id("com.gradleup.shadow")
application
`maven-publish`
signing
Expand Down Expand Up @@ -107,12 +112,25 @@ dependencies {
// -- APK Parsing (GUI) -------------------------------------------------
implementation(libs.apk.parser)

implementation(libs.about.libraries.core)
implementation(libs.about.libraries.m3)

// -- Testing -----------------------------------------------------------
testImplementation(libs.kotlin.test)
testImplementation(libs.junit.params)
testImplementation(libs.mockk)
}

aboutLibraries {
collect {
configPath = file("aboutlibraries")
}
library {
duplicationMode = DuplicateMode.MERGE
duplicationRule = DuplicateRule.EXACT
}
}

// ============================================================================
// Tasks
// ============================================================================
Expand All @@ -134,10 +152,20 @@ tasks {
}

processResources {
// Make sure the licenses are generated before the resources are processed
dependsOn("exportLibraryDefinitions")
from(layout.buildDirectory.file("generated/aboutLibraries/aboutlibraries.json"))

// Only expand properties files, not binary files like PNG/ICO
filesMatching("**/*.properties") {
expand("projectVersion" to project.version)
}
// Bundle the project's NOTICE (GPL 7b/7c) and LICENSE into META-INF so they
// land in the main JAR before the Shadow merge. Source of truth stays at the
// repo root — these are copied at build time, not duplicated in source control.
from(arrayOf(rootProject.file("NOTICE"), rootProject.file("LICENSE"))) {
into("META-INF")
}
}

// -------------------------------------------------------------------------
Expand All @@ -149,6 +177,32 @@ tasks {
"/prebuilt/windows/aapt.exe",
"/prebuilt/*/aapt_*",
)

// NOTICE/LICENSE handling:
// * Global strategy is EXCLUDE (first-wins) so duplicates at non-transformed
// paths — including native libs like libskiko-*.dylib — are deduplicated.
// INCLUDE globally would double-pack every colliding resource and bloat the
// JAR by tens of MB.
// * For META-INF/NOTICE* paths specifically, strategy is flipped to INCLUDE
// via filesMatching below so all dep NOTICEs reach NoticeMergeTransformer
// (Shadow drops duplicates before transformers run under EXCLUDE — see
// ShadowJar.kt Kdoc).
// * Root /NOTICE and /LICENSE — our project's files, added below via from().
// With EXCLUDE, the first occurrence wins. Dep JARs with root-level NOTICE/
// LICENSE lose because our from() block is declared before Shadow processes
// dependency configurations.
// * META-INF/LICENSE — our GPL LICENSE, placed via processResources so it
// lands in the main JAR ahead of dep copies. Dep LICENSE files at unique
// paths (META-INF/androidx/**/LICENSE.txt, etc.) are preserved untouched.
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
filesMatching(listOf(
"META-INF/NOTICE",
"META-INF/NOTICE.txt",
"META-INF/NOTICE.md",
)) {
duplicatesStrategy = DuplicatesStrategy.INCLUDE
}
from(rootProject.file("NOTICE"), rootProject.file("LICENSE"))
minimize {
exclude(dependency("org.bouncycastle:.*"))
exclude(dependency("app.morphe:morphe-patcher"))
Expand All @@ -163,6 +217,16 @@ tasks {
}

mergeServiceFiles()

// Concatenate every META-INF/NOTICE (and .txt/.md variants) from all dep JARs
// plus our own into a single merged file. Satisfies Apache 2.0 §4(d) which
// requires preserving attribution NOTICEs of Apache-licensed dependencies.
//
// Shadow's built-in ApacheNoticeResourceTransformer hardcodes ASF-branded
// copyright text that cannot be fully disabled, which would falsely attribute
// this GPL project to the Apache Software Foundation. NoticeMergeTransformer
// (in buildSrc) is a minimal verbatim concatenator with no boilerplate.
transform(NoticeMergeTransformer::class.java)
}

distTar {
Expand Down
17 changes: 17 additions & 0 deletions buildSrc/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
plugins {
java
}

repositories {
mavenCentral()
gradlePluginPortal()
}

dependencies {
// Shadow is declared as implementation so the root project's build script
// gets these classes on its buildscript classpath at runtime. The root
// project must then apply the shadow plugin without a version to avoid
// the "plugin is already on the classpath" conflict.
implementation("com.gradleup.shadow:shadow-gradle-plugin:9.3.2")
implementation("org.jetbrains:annotations:24.1.0")
}
115 changes: 115 additions & 0 deletions buildSrc/src/main/java/NoticeMergeTransformer.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import com.github.jengelman.gradle.plugins.shadow.transformers.CacheableTransformer;
import com.github.jengelman.gradle.plugins.shadow.transformers.ResourceTransformer;
import com.github.jengelman.gradle.plugins.shadow.transformers.TransformerContext;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.List;
import javax.inject.Inject;
import org.apache.tools.zip.ZipEntry;
import org.apache.tools.zip.ZipOutputStream;
import org.gradle.api.file.FileTreeElement;
import org.gradle.api.model.ObjectFactory;
import org.gradle.api.provider.ListProperty;
import org.gradle.api.provider.Property;
import org.gradle.api.tasks.Input;
import org.gradle.api.tasks.Internal;
import org.jetbrains.annotations.NotNull;

/**
* Concatenates matched resource files across dependency JARs into a single
* output entry. Unlike Shadow's ApacheNoticeResourceTransformer this injects
* no ASF-branded boilerplate, which is required for a GPL project that must
* preserve third-party NOTICEs verbatim without falsely attributing itself
* to the Apache Software Foundation.
*
* Path matching is case-insensitive so NOTICE / NOTICE.txt / NOTICE.md are
* all merged into the same output file. Shadow processes main source set
* resources before dependency JARs, so when the project's NOTICE is shipped
* via processResources it naturally appears first in the merged output.
*
* Written in Java to avoid Kotlin metadata version conflicts between the
* buildSrc compiler and the Shadow plugin classes.
*/
@CacheableTransformer
public abstract class NoticeMergeTransformer implements ResourceTransformer {

private static final String DEFAULT_SEPARATOR =
"\n\n----------------------------------------------------------------\n\n";
private static final List<String> DEFAULT_PATHS = Arrays.asList(
"META-INF/NOTICE",
"META-INF/NOTICE.txt",
"META-INF/NOTICE.md"
);

private final ObjectFactory objectFactory;
private final ByteArrayOutputStream buffer = new ByteArrayOutputStream();
private int sectionCount = 0;

@Inject
public NoticeMergeTransformer(ObjectFactory objectFactory) {
this.objectFactory = objectFactory;
getOutputPath().convention("META-INF/NOTICE");
getMatchedPaths().convention(DEFAULT_PATHS);
getSeparator().convention(DEFAULT_SEPARATOR);
}

@Input
public abstract Property<String> getOutputPath();

@Input
public abstract ListProperty<String> getMatchedPaths();

@Input
public abstract Property<String> getSeparator();

@Internal
@NotNull
@Override
public ObjectFactory getObjectFactory() {
return objectFactory;
}

@Override
public boolean canTransformResource(@NotNull FileTreeElement element) {
String path = element.getRelativePath().getPathString();
for (String matched : getMatchedPaths().get()) {
if (matched.equalsIgnoreCase(path)) {
return true;
}
}
return false;
}

@Override
public void transform(@NotNull TransformerContext context) throws IOException {
if (sectionCount > 0) {
buffer.write(getSeparator().get().getBytes(StandardCharsets.UTF_8));
}
byte[] readBuf = new byte[8192];
int n;
while ((n = context.getInputStream().read(readBuf)) != -1) {
buffer.write(readBuf, 0, n);
}
sectionCount++;
}

@Override
public boolean hasTransformedResource() {
return sectionCount > 0;
}

@Override
public void modifyOutputStream(@NotNull ZipOutputStream os, boolean preserveFileTimestamps) throws IOException {
ZipEntry entry = new ZipEntry(getOutputPath().get());
if (!preserveFileTimestamps) {
entry.setTime(0);
}
os.putNextEntry(entry);
os.write(buffer.toByteArray());
os.closeEntry();
buffer.reset();
sectionCount = 0;
}
}
2 changes: 1 addition & 1 deletion docs/0_prerequisites.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ To use Morphe CLI, you will need to fulfill specific requirements.

## 🤝 Requirements

- Java Runtime Environment 17 - [Azul Zulu JRE](https://www.azul.com/downloads/?version=java-17-lts&package=jre#zulu) or [OpenJDK](https://jdk.java.net/archive/)
- Java Runtime Environment 21 - [Azul Zulu JRE](https://www.azul.com/downloads/?version=java-21-lts&package=jre#zulu) or [OpenJDK](https://jdk.java.net/archive/)
- [Android Debug Bridge (ADB)](https://developer.android.com/studio/command-line/adb) if you want to install the patched APK file on your device

## ⏭️ Whats next
Expand Down
2 changes: 1 addition & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
org.gradle.parallel = true
org.gradle.caching = true
kotlin.code.style = official
version = 1.7.0
version = 1.8.0-dev.6
14 changes: 10 additions & 4 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
@@ -1,17 +1,16 @@
[versions]
# Core
shadow = "9.3.2"
junit = "5.11.0"
kotlin = "2.3.20"

# CLI
picocli = "4.7.7"
arsclib = "9696ffecda"
morphe-patcher = "1.3.3"
morphe-patcher = "1.4.2"
morphe-library = "1.3.0"

# Compose Desktop
compose = "1.10.0"
compose = "1.10.3"

# Networking
ktor = "3.4.2"
Expand All @@ -35,6 +34,9 @@ apk-parser = "2.6.10"
# Testing
mockk = "1.14.9"

# Libraries
about-libraries = "14.0.1"

[libraries]
# Morphe Core
arsclib = { module = "com.github.MorpheApp:ARSCLib", version.ref = "arsclib" }
Expand Down Expand Up @@ -79,9 +81,13 @@ apk-parser = { module = "net.dongliu:apk-parser", version.ref = "apk-parser" }
kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" }
mockk = { module = "io.mockk:mockk", version.ref = "mockk" }

# About Libraries
about-libraries-core = { group = "com.mikepenz", name = "aboutlibraries-compose-core", version.ref = "about-libraries" }
about-libraries-m3 = { group = "com.mikepenz", name = "aboutlibraries-compose-m3", version.ref = "about-libraries" }

[plugins]
kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }
kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
compose = { id = "org.jetbrains.compose", version.ref = "compose" }
shadow = { id = "com.gradleup.shadow", version.ref = "shadow" }
about-libraries = { id = "com.mikepenz.aboutlibraries.plugin", version.ref = "about-libraries" }
Loading
Loading