diff options
Diffstat (limited to 'cmd/incremental_dex_input')
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) +} |
