aboutsummaryrefslogtreecommitdiff
path: root/cmd/incremental_dex_input
diff options
context:
space:
mode:
Diffstat (limited to 'cmd/incremental_dex_input')
-rw-r--r--cmd/incremental_dex_input/Android.bp26
-rw-r--r--cmd/incremental_dex_input/README.md43
-rw-r--r--cmd/incremental_dex_input/incremental_dex_input_lib/Android.bp29
-rw-r--r--cmd/incremental_dex_input/incremental_dex_input_lib/generate_incremental_input.go194
-rw-r--r--cmd/incremental_dex_input/incremental_dex_input_lib/generate_incremental_input_test.go493
-rw-r--r--cmd/incremental_dex_input/main.go55
6 files changed, 840 insertions, 0 deletions
diff --git a/cmd/incremental_dex_input/Android.bp b/cmd/incremental_dex_input/Android.bp
new file mode 100644
index 000000000..b68a4e69b
--- /dev/null
+++ b/cmd/incremental_dex_input/Android.bp
@@ -0,0 +1,26 @@
+// Copyright (C) 2025 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+package {
+ default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+blueprint_go_binary {
+ name: "incremental_dex_input",
+ srcs: [
+ "main.go",
+ ],
+ deps: [
+ "soong-cmd-incremental_dex_input-lib",
+ ],
+}
diff --git a/cmd/incremental_dex_input/README.md b/cmd/incremental_dex_input/README.md
new file mode 100644
index 000000000..436a1e0be
--- /dev/null
+++ b/cmd/incremental_dex_input/README.md
@@ -0,0 +1,43 @@
+// Copyright (C) 2025 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+# Incremental Dex Input
+
+[incremental_dex_input] command line tool. This tool can be used to find the correct subset
+of java packages to be passed for incremental dexing
+
+# Getting Started
+
+## Inputs
+* class jar, jar file containing java class files to be dexed.
+* deps file, containing the dependencies for dex.
+* dexTarget path to the output of ninja rule that triggers dex
+* outputDir, path to a output dir where dex outputs are placed
+
+## Output
+* [dexTarget].rsp file, representing list of all java packages
+* [dexTarget].inc.rsp file, representing list of java packages to be incrementally dexed
+* [dexTarget].input.pc_state.new temp state file, representing the current state of all dex sources (java class files)
+* [dexTarget].deps.pc_state.new temp state file, representing the current state of dex dependencies.
+
+## Usage
+```
+incremental_dex_input --classesJar [classJar] --dexTarget [dexTargetPath] --deps [depsRspFile] --outputDir [outputDirPath]
+```
+
+## Notes
+* This tool internally references the core logic of [find_input_delta] tool.
+* All outputs are relative to the dexTarget path
+* Same class jar, deps, when used for different targets will output *different* results.
+* Once dex succeeds, the temp state files should be saved as current state files, to prepare for next iteration.
diff --git a/cmd/incremental_dex_input/incremental_dex_input_lib/Android.bp b/cmd/incremental_dex_input/incremental_dex_input_lib/Android.bp
new file mode 100644
index 000000000..b5ebfe63b
--- /dev/null
+++ b/cmd/incremental_dex_input/incremental_dex_input_lib/Android.bp
@@ -0,0 +1,29 @@
+// Copyright (C) 2025 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package {
+ default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+bootstrap_go_package {
+ name: "soong-cmd-incremental_dex_input-lib",
+ pkgPath: "android/soong/cmd/incremental_dex_input/incremental_dex_input_lib",
+ deps: [
+ "blueprint-pathtools",
+ "soong-cmd-find_input_delta-lib",
+ ],
+ srcs: [
+ "generate_incremental_input.go",
+ ],
+}
diff --git a/cmd/incremental_dex_input/incremental_dex_input_lib/generate_incremental_input.go b/cmd/incremental_dex_input/incremental_dex_input_lib/generate_incremental_input.go
new file mode 100644
index 000000000..20d918730
--- /dev/null
+++ b/cmd/incremental_dex_input/incremental_dex_input_lib/generate_incremental_input.go
@@ -0,0 +1,194 @@
+package incremental_dex_input_lib
+
+import (
+ "archive/zip"
+ "fmt"
+ "os"
+ "path/filepath"
+ "regexp"
+ "sort"
+ "strings"
+
+ fid_lib "android/soong/cmd/find_input_delta/find_input_delta_lib"
+)
+
+var fileSepRegex = regexp.MustCompile("[^[:space:]]+")
+
+func GenerateIncrementalInput(jarFilePath, outputDir, packageOutputDir, dexTarget, deps string) {
+ inputPcState := dexTarget + ".input.pc_state"
+ depsPcState := dexTarget + ".deps.pc_state"
+
+ packagePaths := getAllPackages(jarFilePath)
+ var chPackagePaths []string
+ includeAllPackages := false
+
+ addF, delF, chF := findInputDelta([]string{jarFilePath}, inputPcState, dexTarget, true)
+
+ depsList := readRspFile(deps)
+ addD, delD, chD := findInputDelta(depsList, depsPcState, dexTarget, false)
+
+ // If we are not doing a partial compile, we can just return all packages in the incremental list.
+ if !usePartialCompile() {
+ includeAllPackages = true
+ chPackagePaths = packagePaths
+ }
+
+ // Changing the dependencies warrants including all packages.
+ if !includeAllPackages && len(addD)+len(delD)+len(chD) > 0 {
+ includeAllPackages = true
+ chPackagePaths = packagePaths
+ }
+
+ if !includeAllPackages {
+ chPackageSet := make(map[string]bool)
+ chPackagePaths = nil
+ // We only want to include all packages when there is a modification to the number of packages present.
+ // We loosely simulate that by including all packages when there is change in number of classes in the jar.
+ if len(addF) > 0 || len(delF) > 0 {
+ includeAllPackages = true
+ chPackagePaths = packagePaths
+ } else {
+ for _, ch := range chF {
+ // Filter out the files that do not end with ".class"
+ if normalizedPath := getPackagePath(ch); normalizedPath != "" {
+ chPackageSet[normalizedPath] = true
+ }
+ }
+
+ for path := range chPackageSet {
+ chPackagePaths = append(chPackagePaths, path)
+ }
+ sort.Strings(chPackagePaths)
+ }
+ }
+
+ preparePackagePaths(packageOutputDir, packagePaths)
+
+ writePackagePathsToRspFile(dexTarget+".rsp", packagePaths)
+ writePackagePathsToRspFile(dexTarget+".inc.rsp", chPackagePaths)
+}
+
+// Reads a rsp file and returns the content
+func readRspFile(rspFile string) (list []string) {
+ data, err := os.ReadFile(rspFile)
+ if err != nil {
+ panic(err)
+ }
+ list = append(list, fileSepRegex.FindAllString(string(data), -1)...)
+ return list
+}
+
+// Checks if Full Compile is enabled or not
+func usePartialCompile() bool {
+ usePartialCompileVar := os.Getenv("SOONG_USE_PARTIAL_COMPILE")
+ if usePartialCompileVar == "true" {
+ return true
+ }
+ return false
+}
+
+// Re-creates the package paths.
+func preparePackagePaths(packageOutputDir string, packagePaths []string) {
+ // Create package directories relative to packageOutputDir
+ for _, pkgPath := range packagePaths {
+ targetPath := filepath.Join(packageOutputDir, pkgPath)
+ if err := os.MkdirAll(targetPath, 0755); err != nil {
+ fmt.Println("err: ", err)
+ panic(err)
+ }
+ }
+}
+
+// Returns the list of java packages derived from .class files in a jar.
+func getAllPackages(jarFilePath string) []string {
+ // Open the JAR file for reading.
+ r, err := zip.OpenReader(jarFilePath)
+ if err != nil {
+ panic(err)
+ }
+ defer r.Close()
+
+ packageSet := make(map[string]bool)
+
+ // Iterate over each file in the JAR archive.
+ for _, file := range r.File {
+ if file.FileInfo().IsDir() {
+ continue
+ }
+ if packagePath := getPackagePath(file.Name); packagePath != "" {
+ packageSet[packagePath] = true
+ }
+ }
+
+ packagePaths := make([]string, 0, len(packageSet))
+ for path := range packageSet {
+ packagePaths = append(packagePaths, path)
+ }
+ sort.Strings(packagePaths)
+
+ return packagePaths
+}
+
+// Returns package path, for files ending with .class
+func getPackagePath(file string) string {
+ if strings.HasSuffix(file, ".class") {
+ dirPath := filepath.Dir(file)
+ if dirPath != "." {
+ return filepath.ToSlash(dirPath)
+ }
+ // Return `.` if the class does not have a package, i.e. present at the root
+ // of the jar.
+ return "."
+ }
+ return ""
+}
+
+// writePathsToRspFile writes a slice of strings to a file, one string per line.
+func writePackagePathsToRspFile(filePath string, packagePaths []string) {
+ // Join the paths with newline characters.
+ // Add a final newline for standard text file format.
+ content := strings.Join(packagePaths, "\n") + "\n"
+
+ // Write the content to the file.
+ err := os.WriteFile(filePath, []byte(content), 0644) // 0644: rw-r--r--
+ if err != nil {
+ fmt.Println("failed to write rsp file ", filePath, err)
+ panic(err)
+ }
+}
+
+// Computes the diff of the inputs provided, saving the temp state in the
+// priorStateFile.
+func findInputDelta(inputs []string, priorStateFile, target string, inspect bool) ([]string, []string, []string) {
+ newStateFile := priorStateFile + ".new"
+ fileList, err := fid_lib.GenerateFileList(target, priorStateFile, newStateFile, inputs, inspect, fid_lib.OsFs)
+ if err != nil {
+ panic(err)
+ }
+ return flattenChanges(fileList)
+}
+
+// Recursively flattens the output of find_input_delta.
+func flattenChanges(root *fid_lib.FileList) ([]string, []string, []string) {
+ var allAdditions []string
+ var allDeletions []string
+ var allChangedFiles []string
+
+ for _, addition := range root.Additions {
+ allAdditions = append(allAdditions, addition)
+ }
+
+ for _, del := range root.Deletions {
+ allDeletions = append(allDeletions, del)
+ }
+
+ for _, ch := range root.Changes {
+ allChangedFiles = append(allChangedFiles, ch.Name)
+ recAdd, recDel, recCh := flattenChanges(&ch)
+ allAdditions = append(allAdditions, recAdd...)
+ allDeletions = append(allDeletions, recDel...)
+ allChangedFiles = append(allChangedFiles, recCh...)
+ }
+
+ return allAdditions, allDeletions, allChangedFiles
+}
diff --git a/cmd/incremental_dex_input/incremental_dex_input_lib/generate_incremental_input_test.go b/cmd/incremental_dex_input/incremental_dex_input_lib/generate_incremental_input_test.go
new file mode 100644
index 000000000..a6af57a05
--- /dev/null
+++ b/cmd/incremental_dex_input/incremental_dex_input_lib/generate_incremental_input_test.go
@@ -0,0 +1,493 @@
+package incremental_dex_input_lib
+
+import (
+ "archive/zip"
+ "fmt"
+ "os"
+ "path/filepath"
+ "reflect"
+ "sort"
+ "strings"
+ "testing"
+ "time"
+
+ soong_zip "android/soong/zip"
+
+ fid_lib "android/soong/cmd/find_input_delta/find_input_delta_lib"
+)
+
+// --- Tests for `readRspFile` ---
+
+func TestReadRspFile(t *testing.T) {
+ tmpDir := t.TempDir()
+
+ testCases := []struct {
+ name string
+ content string
+ expected []string
+ }{
+ {"Empty", "", nil},
+ {"SingleLine", "external/kotlinx.serialization/rules/r8.pro", []string{"external/kotlinx.serialization/rules/r8.pro"}},
+ {"MultipleLines", "external/kotlinx.serialization/rules/r8.pro\nfoo/bar/baz/guava.jar\nfoo/bar/baz1/guava1.jar", []string{"external/kotlinx.serialization/rules/r8.pro", "foo/bar/baz/guava.jar", "foo/bar/baz1/guava1.jar"}},
+ {"WithSpaces", " foo/bar/baz/guava.jar \n foo/bar/baz1/guava1.jar ", []string{"foo/bar/baz/guava.jar", "foo/bar/baz1/guava1.jar"}}, //Should be trimmed.
+ {"WithEmptyLines", "foo/bar/baz/guava.jar\n\nfoo/bar/baz1/guava1.jar", []string{"foo/bar/baz/guava.jar", "foo/bar/baz1/guava1.jar"}},
+ {"WithCarriageReturn", "foo/bar/baz/guava.jar\r\nfoo/bar/baz1/guava1.jar", []string{"foo/bar/baz/guava.jar", "foo/bar/baz1/guava1.jar"}},
+ }
+
+ for _, tc := range testCases {
+ t.Run(tc.name, func(t *testing.T) {
+ rspFile := filepath.Join(tmpDir, "test.rsp")
+ writeFile(t, rspFile, tc.content)
+
+ actual := readRspFile(rspFile)
+ if !reflect.DeepEqual(actual, tc.expected) {
+ t.Errorf("readRspFile(); expected %v, got %v", tc.expected, actual)
+ }
+ })
+ }
+
+ //Test for panic
+ t.Run("Panic on non-existent file", func(t *testing.T) {
+ defer func() {
+ if r := recover(); r == nil { //should have panicked
+ t.Errorf("readRspFile() did not panic on non-existent file")
+ }
+ }()
+ _ = readRspFile("nonexistent_file.rsp")
+ })
+}
+
+// --- Tests for `flattenChanges` ---
+
+func TestFlattenChanges(t *testing.T) {
+ fileList := &fid_lib.FileList{
+ Changes: []fid_lib.FileList{
+ {
+ Name: "out/soong/dummy/my/fav/code-target.jar",
+ Additions: []string{"foo/bar/added.class", "foo/bar/added$1.class"},
+ Deletions: []string{"foo/bar/deleted.class"},
+ Changes: []fid_lib.FileList{
+ {Name: "foo/bar/changed.class"},
+ {Name: "foo/bar/changed$1.class"},
+ },
+ },
+ },
+ }
+
+ expectedAdditions := []string{"foo/bar/added.class", "foo/bar/added$1.class"}
+ expectedDeletions := []string{"foo/bar/deleted.class"}
+ expectedChanges := []string{"out/soong/dummy/my/fav/code-target.jar", "foo/bar/changed.class", "foo/bar/changed$1.class"}
+
+ actualAdditions, actualDeletions, actualChanges := flattenChanges(fileList)
+
+ // Sort for consistent comparison
+ sort.Strings(expectedAdditions)
+ sort.Strings(actualAdditions)
+ sort.Strings(expectedDeletions)
+ sort.Strings(actualDeletions)
+ sort.Strings(expectedChanges)
+ sort.Strings(actualChanges)
+
+ if !reflect.DeepEqual(actualAdditions, expectedAdditions) {
+ t.Errorf("flattenChanges() additions; expected %v, got %v", expectedAdditions, actualAdditions)
+ }
+ if !reflect.DeepEqual(actualDeletions, expectedDeletions) {
+ t.Errorf("flattenChanges() deletions; expected %v, got %v", expectedDeletions, actualDeletions)
+ }
+ if !reflect.DeepEqual(actualChanges, expectedChanges) {
+ t.Errorf("flattenChanges() changes; expected %v, got %v", expectedChanges, actualChanges)
+ }
+}
+
+// --- Tests for `getAllPackages` ---
+func TestGetAllPackages(t *testing.T) {
+ testCases := []struct {
+ name string
+ entries map[string][]byte
+ expectedPackages []string // Must be sorted
+ }{
+ {
+ name: "Basic Functionality",
+ entries: map[string][]byte{
+ "com/example/Main.class": nil,
+ "org/gradle/Utils.class": nil,
+ "com/example/data/Model.class": nil,
+ "META-INF/MANIFEST.MF": []byte("Manifest-Version: 1.0"),
+ "config.properties": []byte("key=value"),
+ "com/example/another/One.class": nil,
+ },
+ expectedPackages: []string{"com/example", "com/example/another", "com/example/data", "org/gradle"},
+ },
+ {
+ name: "Empty JAR",
+ entries: map[string][]byte{},
+ expectedPackages: []string{},
+ },
+ {
+ name: "No Class Files",
+ entries: map[string][]byte{
+ "META-INF/MANIFEST.MF": []byte("Manifest-Version: 1.0"),
+ "resource.txt": []byte("some data"),
+ "some/dir/config.xml": nil,
+ },
+ expectedPackages: []string{},
+ },
+ {
+ name: "Root Class Files",
+ entries: map[string][]byte{
+ "RootClass.class": nil,
+ "AnotherRoot.class": nil,
+ "com/example/App.class": nil,
+ "org/myapp/Start.class": nil,
+ },
+ expectedPackages: []string{".", "com/example", "org/myapp"},
+ },
+ {
+ name: "Duplicate Packages",
+ entries: map[string][]byte{
+ "com/example/ClassA.class": nil,
+ "com/example/ClassB.class": nil,
+ "org/utils/Helper1.class": nil,
+ "org/utils/Helper2.class": nil,
+ "com/example/ClassC.class": nil,
+ },
+ expectedPackages: []string{"com/example", "org/utils"},
+ },
+ {
+ name: "Nested Packages",
+ entries: map[string][]byte{
+ "com/example/util/io/Reader.class": nil,
+ "com/example/util/net/Client.class": nil,
+ "com/example/App.class": nil,
+ },
+ expectedPackages: []string{"com/example", "com/example/util/io", "com/example/util/net"},
+ },
+ }
+
+ for _, tc := range testCases {
+ tc := tc // Capture range variable
+ t.Run(tc.name, func(t *testing.T) {
+ jarFileName := strings.ReplaceAll(strings.ToLower(tc.name), " ", "_") + ".jar"
+ jarPath := createTestJar(t, jarFileName, tc.entries)
+
+ actualPackages := getAllPackages(jarPath)
+
+ // --- Assertion using standard testing package ---
+ if !reflect.DeepEqual(tc.expectedPackages, actualPackages) {
+ t.Errorf("Test case '%s' failed:\nExpected: %v\nActual: %v",
+ tc.name, tc.expectedPackages, actualPackages)
+ }
+ if len(tc.expectedPackages) != len(actualPackages) {
+ t.Errorf("Test case '%s' failed: Expected length %d, got %d",
+ tc.name, len(tc.expectedPackages), len(actualPackages))
+ }
+ })
+ }
+}
+
+// --- Test Fixture Setup ---
+// Struct to hold common test file paths
+type testFixture struct {
+ t *testing.T
+ tmpDir string
+ DexOutputDir string
+ PackageOutputDir string
+ ClassJar string
+ DepsRspFile string
+ DexTargetJar string
+ ClassFile1 string
+ ClassFile2 string
+ ClassFile3 string
+ DepJar string
+}
+
+// newTestFixture creates the temporary directory and necessary files
+func newTestFixture(t *testing.T) *testFixture {
+ tmpDir := t.TempDir() // Use t.TempDir for automatic cleanup
+
+ // Create dummy files needed for the tests
+ fixture := &testFixture{
+ t: t,
+ tmpDir: tmpDir,
+ DexOutputDir: filepath.Join(tmpDir, "dex"),
+ PackageOutputDir: filepath.Join(tmpDir, "dex", "packages"),
+ ClassJar: filepath.Join(tmpDir, "javac/classes.jar"),
+ DepsRspFile: filepath.Join(tmpDir, "dex/deps.rsp"),
+ DexTargetJar: filepath.Join(tmpDir, "dex/dex.jar"),
+ ClassFile1: filepath.Join(tmpDir, "javac/classes/com/example/ClassA.class"),
+ ClassFile2: filepath.Join(tmpDir, "javac/classes/com/example/ClassC.class"),
+ ClassFile3: filepath.Join(tmpDir, "javac/classes/org/another/ClassD.class"),
+ DepJar: filepath.Join(tmpDir, "dex/deps.jar"),
+ }
+
+ // Create directories and initial file contents
+ createDir(t, filepath.Dir(fixture.ClassFile1))
+ createDir(t, filepath.Dir(fixture.ClassFile3))
+ createDir(t, fixture.DexOutputDir)
+
+ writeFile(t, fixture.ClassFile1, "package com.example; class File1 {}")
+ writeFile(t, fixture.ClassFile2, "package com.example; class File2 {}")
+ writeFile(t, fixture.ClassFile3, "package org.another; class ClassD {}")
+
+ writeFile(t, fixture.DepJar, "Dep jar")
+
+ writeFile(t, fixture.DepsRspFile, fmt.Sprintf("%s", fixture.DepJar))
+ writeFile(t, fixture.DexTargetJar, "Dex Jar")
+
+ return fixture
+}
+
+// --- Tests for `generateIncrementalInput` ---
+
+func TestGenerateIncrementalInput(t *testing.T) {
+ // Set the environment variable to enable inc-compilation
+ t.Setenv("SOONG_USE_PARTIAL_COMPILE", "true")
+
+ // Shared setup for all subtests
+ tf := newTestFixture(t)
+
+ // --- Subtest: Initial Full Compile ---
+ t.Run("InitialFullCompile", func(t *testing.T) {
+ // Arrange
+ createQualifiedTestJar(t, tf.ClassJar, filepath.Join(tf.tmpDir, "javac", "classes"))
+
+ // Act
+ tf.runGenerator()
+
+ // Assert
+ checkOutput(
+ t,
+ tf.incOutputPath(),
+ fmt.Sprintf("%s\n%s", "com/example", "org/another"), // All files included initially
+ )
+ tf.savePriorState()
+ })
+
+ // --- Subtest: Incremental - One Class File Modified ---
+ t.Run("Incremental_OneFileModified", func(t *testing.T) {
+ // Arrange: Modify one file (ensure timestamp changes)
+ modifyFile(t, tf.ClassFile1, "Incremental_OneFileModified")
+ createQualifiedTestJar(t, tf.ClassJar, filepath.Join(tf.tmpDir, "javac", "classes"))
+
+ // Act
+ tf.runGenerator()
+
+ // Assert
+ checkOutput(
+ t,
+ tf.incOutputPath(),
+ fmt.Sprintf("%s", "com/example"),
+ )
+ tf.savePriorState()
+ })
+
+ // --- Subtest: Incremental - Dependency Change ---
+ t.Run("Incremental_DependencyChanged", func(t *testing.T) {
+ // Arrange: Modify the DepJar
+ modifyFile(t, tf.DepJar, "Incremental_DependencyChanged")
+ createQualifiedTestJar(t, tf.ClassJar, filepath.Join(tf.tmpDir, "javac", "classes"))
+
+ // Act
+ tf.runGenerator()
+
+ // Assert: All source files should be in inc.rsp
+ checkOutput(
+ t,
+ tf.incOutputPath(),
+ fmt.Sprintf("%s\n%s", "com/example", "org/another"),
+ )
+ tf.savePriorState()
+ })
+
+ // --- Subtest: Incremental - One Class File Added ---
+ t.Run("Incremental_FileAdded", func(t *testing.T) {
+ // Arrange: Add one class file
+ writeFile(t, filepath.Join(tf.tmpDir, "javac/classes/org/another/ClassE.class"), "package org.another; class File4 {}")
+ createQualifiedTestJar(t, tf.ClassJar, filepath.Join(tf.tmpDir, "javac", "classes"))
+
+ // Act
+ tf.runGenerator()
+
+ // Assert: Check usages of deleted file in inc.rsp, and class files
+ // corresponding to deleted files in rem.rsp
+ checkOutput(
+ t,
+ tf.incOutputPath(),
+ fmt.Sprintf("%s\n%s", "com/example", "org/another"),
+ )
+ tf.savePriorState() // Save state if needed for subsequent tests
+ })
+}
+
+// --- Tests for `generateIncrementalInputPartialCompileOff` ---
+func TestGenerateIncrementalInputPartialCompileOff(t *testing.T) {
+ // Set the environment variable to enable inc-compilation
+ t.Setenv("SOONG_USE_PARTIAL_COMPILE", "")
+
+ // Shared setup for all subtests
+ tf := newTestFixture(t)
+
+ // --- Subtest: Initial Full Compile ---
+ t.Run("InitialFullCompile", func(t *testing.T) {
+ // Arrange
+ createQualifiedTestJar(t, tf.ClassJar, filepath.Join(tf.tmpDir, "javac", "classes"))
+
+ // Act
+ tf.runGenerator()
+
+ // Assert
+ checkOutput(
+ t,
+ tf.incOutputPath(),
+ fmt.Sprintf("%s\n%s", "com/example", "org/another"), // All files included initially
+ )
+ tf.savePriorState()
+ })
+
+ // --- Subtest: Incremental - One Class File Modified ---
+ t.Run("Incremental_OneFileModified", func(t *testing.T) {
+ // Arrange: Modify one file (ensure timestamp changes)
+ modifyFile(t, tf.ClassFile1, "Incremental_OneFileModified")
+ createQualifiedTestJar(t, tf.ClassJar, filepath.Join(tf.tmpDir, "javac", "classes"))
+
+ // Act
+ tf.runGenerator()
+
+ // Assert
+ checkOutput(
+ t,
+ tf.incOutputPath(),
+ fmt.Sprintf("%s\n%s", "com/example", "org/another"),
+ )
+ tf.savePriorState()
+ })
+}
+
+// createQualifiedTestJar creates a jar using soong_zip, mimicking javac
+func createQualifiedTestJar(t *testing.T, outputPath, inputDir string) {
+ err := soong_zip.Zip(soong_zip.ZipArgs{
+ EmulateJar: true,
+ OutputFilePath: outputPath,
+ FileArgs: soong_zip.NewFileArgsBuilder().SourcePrefixToStrip(inputDir).Dir(inputDir).FileArgs(),
+ })
+ if err != nil {
+ t.Fatalf("Error creating jar %s: %v", outputPath, err)
+ }
+}
+
+// runGenerator calls GenerateIncrementalInput for the testFixture
+func (tf *testFixture) runGenerator() {
+ // Small delay often needed for filesystem timestamp granularity
+ time.Sleep(15 * time.Millisecond)
+ GenerateIncrementalInput(tf.ClassJar, tf.DexOutputDir, tf.PackageOutputDir, tf.DexTargetJar, tf.DepsRspFile)
+}
+
+// returns incOutputPath for testFixture
+func (tf *testFixture) incOutputPath() string {
+ return tf.DexTargetJar + ".inc.rsp"
+}
+
+// Helper to save prior state
+func (tf *testFixture) savePriorState() {
+ tf.t.Helper()
+ // Implement your logic to save the necessary state files
+ // e.g., copy *.pc_state.new to *.pc_state
+ inputStateNew := tf.DexTargetJar + ".input.pc_state.new"
+ inputState := tf.DexTargetJar + ".input.pc_state"
+ depsStateNew := tf.DexTargetJar + ".deps.pc_state.new"
+ depsState := tf.DexTargetJar + ".deps.pc_state"
+
+ os.Rename(inputStateNew, inputState)
+ os.Rename(depsStateNew, depsState)
+}
+
+// Verifies the test output against expected output
+func checkOutput(t *testing.T, incOutputPath, expectedIncContent string) {
+ contentBytes, err := os.ReadFile(incOutputPath)
+ if err != nil {
+ t.Fatalf("Failed to read output file %q: %v", incOutputPath, err)
+ }
+ actualContent := strings.TrimSpace(string(contentBytes))
+
+ actualLines := strings.Split(actualContent, "\n")
+ expectedLines := strings.Split(expectedIncContent, "\n")
+ sort.Strings(actualLines)
+ sort.Strings(expectedLines)
+ actualContent = strings.Join(actualLines, "\n")
+ expectedIncContent = strings.Join(expectedLines, "\n")
+
+ if actualContent != expectedIncContent {
+ t.Errorf("Unexpected content in %q.\nGot:\n%s\nWant:\n%s", incOutputPath, actualContent, expectedIncContent)
+ }
+}
+
+// createTestJar creates a temporary JAR file for testing purposes.
+// entries is a map where key is the entry name (path inside JAR) and value is content.
+func createTestJar(t *testing.T, filename string, entries map[string][]byte) string {
+ t.Helper() // Mark this as a test helper
+
+ tempDir := t.TempDir() // Automatically cleaned up
+ fullPath := filepath.Join(tempDir, filename)
+
+ jarFile, err := os.Create(fullPath)
+ if err != nil {
+ t.Fatalf("Failed to create test JAR file %s: %v", fullPath, err)
+ }
+ defer jarFile.Close() // Ensure file is closed
+
+ zipWriter := zip.NewWriter(jarFile)
+ defer zipWriter.Close() // Ensure writer is closed
+
+ for name, content := range entries {
+ name = filepath.ToSlash(name)
+ writer, err := zipWriter.Create(name)
+ if err != nil {
+ t.Fatalf("Failed to create entry %s in test JAR %s: %v", name, filename, err)
+ }
+ if content != nil {
+ _, err = writer.Write(content)
+ if err != nil {
+ t.Fatalf("Failed to write content for entry %s in test JAR %s: %v", name, filename, err)
+ }
+ }
+ }
+
+ err = zipWriter.Close()
+ if err != nil {
+ t.Fatalf("Failed to close zip writer for JAR %s: %v", filename, err)
+ }
+ err = jarFile.Close()
+ if err != nil {
+ t.Fatalf("Failed to close JAR file %s: %v", filename, err)
+ }
+
+ return fullPath
+}
+
+// --- File Create/Mod/Delete helpers ---
+func createDir(t *testing.T, dirPath string) {
+ t.Helper()
+ err := os.MkdirAll(dirPath, 0755)
+ if err != nil {
+ t.Fatalf("Failed to create directory %q: %v", dirPath, err)
+ }
+}
+
+func writeFile(t *testing.T, filePath, content string) {
+ t.Helper()
+ err := os.WriteFile(filePath, []byte(content), 0644)
+ if err != nil {
+ t.Fatalf("Failed to write file %q: %v", filePath, err)
+ }
+}
+
+func modifyFile(t *testing.T, filePath, newContentSuffix string) {
+ t.Helper()
+ // Append suffix to ensure modification time changes reliably
+ contentBytes, err := os.ReadFile(filePath)
+ if err != nil && !os.IsNotExist(err) { // Allow modification even if file was deleted before
+ t.Fatalf("Failed to read file for modification %q: %v", filePath, err)
+ }
+ newContent := string(contentBytes) + "// " + newContentSuffix
+ writeFile(t, filePath, newContent)
+}
diff --git a/cmd/incremental_dex_input/main.go b/cmd/incremental_dex_input/main.go
new file mode 100644
index 000000000..5be86c40c
--- /dev/null
+++ b/cmd/incremental_dex_input/main.go
@@ -0,0 +1,55 @@
+// Copyright (C) 2025 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package main
+
+import (
+ "flag"
+
+ idi_lib "android/soong/cmd/incremental_dex_input/incremental_dex_input_lib"
+)
+
+func main() {
+ var classesJar, deps, outputDir, packageOutputDir, dexTarget string
+
+ flag.StringVar(&classesJar, "classesJar", "", "jar file containing compiled java classes")
+ flag.StringVar(&deps, "deps", "", "rsp file enlisting all module deps")
+ flag.StringVar(&dexTarget, "dexTarget", "", "dex output")
+ flag.StringVar(&outputDir, "outputDir", "", "root directory for creating dex entries")
+ flag.StringVar(&packageOutputDir, "packageOutputDir", "", "root directory for creating package based dex entries")
+
+ flag.Parse()
+
+ if classesJar == "" {
+ panic("must specify --classesJar")
+ }
+
+ if deps == "" {
+ panic("must specify --deps")
+ }
+
+ if dexTarget == "" {
+ panic("must specify --dexTarget")
+ }
+
+ if outputDir == "" {
+ panic("must specify --outputDir")
+ }
+
+ if packageOutputDir == "" {
+ panic("must specify --packageOutputDir")
+ }
+
+ idi_lib.GenerateIncrementalInput(classesJar, outputDir, packageOutputDir, dexTarget, deps)
+}