diff --git a/android/paths.go b/android/paths.go index d6a1c6684..4adaa2d83 100644 --- a/android/paths.go +++ b/android/paths.go @@ -18,6 +18,7 @@ import ( "fmt" "path/filepath" "reflect" + "sort" "strings" "github.com/google/blueprint" @@ -380,6 +381,38 @@ func (p Paths) FilterOutByExt(ext string) Paths { return ret } +// DirectorySortedPaths is a slice of paths that are sorted such that all files in a directory +// (including subdirectories) are in a contiguous subslice of the list, and can be found in +// O(log(N)) time using a binary search on the directory prefix. +type DirectorySortedPaths Paths + +func PathsToDirectorySortedPaths(paths Paths) DirectorySortedPaths { + ret := append(DirectorySortedPaths(nil), paths...) + sort.Slice(ret, func(i, j int) bool { + return ret[i].String() < ret[j].String() + }) + return ret +} + +// PathsInDirectory returns a subslice of the DirectorySortedPaths as a Paths that contains all entries +// that are in the specified directory and its subdirectories. +func (p DirectorySortedPaths) PathsInDirectory(dir string) Paths { + prefix := filepath.Clean(dir) + "/" + start := sort.Search(len(p), func(i int) bool { + return prefix < p[i].String() + }) + + ret := p[start:] + + end := sort.Search(len(ret), func(i int) bool { + return !strings.HasPrefix(ret[i].String(), prefix) + }) + + ret = ret[:end] + + return Paths(ret) +} + // WritablePaths is a slice of WritablePaths, used for multiple outputs. type WritablePaths []WritablePath diff --git a/android/paths_test.go b/android/paths_test.go index 248f4d43a..82ae4dcf7 100644 --- a/android/paths_test.go +++ b/android/paths_test.go @@ -340,3 +340,75 @@ func TestPathForModuleInstall(t *testing.T) { }) } } + +func TestDirectorySortedPaths(t *testing.T) { + makePaths := func() Paths { + return Paths{ + PathForTesting("a.txt"), + PathForTesting("a/txt"), + PathForTesting("a/b/c"), + PathForTesting("a/b/d"), + PathForTesting("b"), + PathForTesting("b/b.txt"), + PathForTesting("a/a.txt"), + } + } + + expected := []string{ + "a.txt", + "a/a.txt", + "a/b/c", + "a/b/d", + "a/txt", + "b", + "b/b.txt", + } + + paths := makePaths() + reversePaths := make(Paths, len(paths)) + for i, v := range paths { + reversePaths[len(paths)-i-1] = v + } + + sortedPaths := PathsToDirectorySortedPaths(paths) + reverseSortedPaths := PathsToDirectorySortedPaths(reversePaths) + + if !reflect.DeepEqual(Paths(sortedPaths).Strings(), expected) { + t.Fatalf("sorted paths:\n %#v\n != \n %#v", paths.Strings(), expected) + } + + if !reflect.DeepEqual(Paths(reverseSortedPaths).Strings(), expected) { + t.Fatalf("sorted reversed paths:\n %#v\n !=\n %#v", reversePaths.Strings(), expected) + } + + expectedA := []string{ + "a/a.txt", + "a/b/c", + "a/b/d", + "a/txt", + } + + inA := sortedPaths.PathsInDirectory("a") + if !reflect.DeepEqual(inA.Strings(), expectedA) { + t.Errorf("FilesInDirectory(a):\n %#v\n != \n %#v", inA.Strings(), expectedA) + } + + expectedA_B := []string{ + "a/b/c", + "a/b/d", + } + + inA_B := sortedPaths.PathsInDirectory("a/b") + if !reflect.DeepEqual(inA_B.Strings(), expectedA_B) { + t.Errorf("FilesInDirectory(a/b):\n %#v\n != \n %#v", inA_B.Strings(), expectedA_B) + } + + expectedB := []string{ + "b/b.txt", + } + + inB := sortedPaths.PathsInDirectory("b") + if !reflect.DeepEqual(inB.Strings(), expectedB) { + t.Errorf("FilesInDirectory(b):\n %#v\n != \n %#v", inA.Strings(), expectedA) + } +} diff --git a/zip/Android.bp b/zip/Android.bp index d7080899b..3bb4f25d6 100644 --- a/zip/Android.bp +++ b/zip/Android.bp @@ -19,6 +19,7 @@ bootstrap_go_package { pkgPath: "android/soong/zip", deps: [ "android-archive-zip", + "blueprint-pathtools", "soong-jar", ], srcs: [ diff --git a/zip/cmd/main.go b/zip/cmd/main.go index 348728c25..c0418f7b5 100644 --- a/zip/cmd/main.go +++ b/zip/cmd/main.go @@ -120,14 +120,15 @@ func (d *dir) Set(s string) error { } var ( - out = flag.String("o", "", "file to write zip file to") - manifest = flag.String("m", "", "input jar manifest file name") - directories = flag.Bool("d", false, "include directories in zip") - rootPrefix = flag.String("P", "", "path prefix within the zip at which to place files") - relativeRoot = flag.String("C", "", "path to use as relative root of files in following -f, -l, or -D arguments") - parallelJobs = flag.Int("j", runtime.NumCPU(), "number of parallel threads to use") - compLevel = flag.Int("L", 5, "deflate compression level (0-9)") - emulateJar = flag.Bool("jar", false, "modify the resultant .zip to emulate the output of 'jar'") + out = flag.String("o", "", "file to write zip file to") + manifest = flag.String("m", "", "input jar manifest file name") + directories = flag.Bool("d", false, "include directories in zip") + rootPrefix = flag.String("P", "", "path prefix within the zip at which to place files") + relativeRoot = flag.String("C", "", "path to use as relative root of files in following -f, -l, or -D arguments") + parallelJobs = flag.Int("j", runtime.NumCPU(), "number of parallel threads to use") + compLevel = flag.Int("L", 5, "deflate compression level (0-9)") + emulateJar = flag.Bool("jar", false, "modify the resultant .zip to emulate the output of 'jar'") + writeIfChanged = flag.Bool("write_if_changed", false, "only update resultant .zip if it has changed") fArgs zip.FileArgs nonDeflatedFiles = make(uniqueSet) @@ -163,6 +164,7 @@ func main() { ManifestSourcePath: *manifest, NumParallelJobs: *parallelJobs, NonDeflatedFiles: nonDeflatedFiles, + WriteIfChanged: *writeIfChanged, }) if err != nil { fmt.Fprintln(os.Stderr, err.Error()) diff --git a/zip/zip.go b/zip/zip.go index 95520fe31..c878a0cce 100644 --- a/zip/zip.go +++ b/zip/zip.go @@ -32,6 +32,8 @@ import ( "sync" "time" + "github.com/google/blueprint/pathtools" + "android/soong/jar" "android/soong/third_party/zip" ) @@ -127,6 +129,7 @@ type ZipArgs struct { ManifestSourcePath string NumParallelJobs int NonDeflatedFiles map[string]bool + WriteIfChanged bool } func Run(args ZipArgs) (err error) { @@ -186,8 +189,38 @@ func Run(args ZipArgs) (err error) { } } - return w.write(args.OutputFilePath, pathMappings, args.ManifestSourcePath, args.EmulateJar, args.NumParallelJobs) + buf := &bytes.Buffer{} + var out io.Writer = buf + if !args.WriteIfChanged { + f, err := os.Create(args.OutputFilePath) + if err != nil { + return err + } + + defer f.Close() + defer func() { + if err != nil { + os.Remove(args.OutputFilePath) + } + }() + + out = f + } + + err = w.write(out, pathMappings, args.ManifestSourcePath, args.EmulateJar, args.NumParallelJobs) + if err != nil { + return err + } + + if args.WriteIfChanged { + err := pathtools.WriteFileIfChanged(args.OutputFilePath, buf.Bytes(), 0666) + if err != nil { + return err + } + } + + return nil } func fillPathPairs(prefix, rel, src string, pathMappings *[]pathMapping, nonDeflatedFiles map[string]bool) error { @@ -226,19 +259,7 @@ type readerSeekerCloser interface { io.Seeker } -func (z *ZipWriter) write(out string, pathMappings []pathMapping, manifest string, emulateJar bool, parallelJobs int) error { - f, err := os.Create(out) - if err != nil { - return err - } - - defer f.Close() - defer func() { - if err != nil { - os.Remove(out) - } - }() - +func (z *ZipWriter) write(f io.Writer, pathMappings []pathMapping, manifest string, emulateJar bool, parallelJobs int) error { z.errors = make(chan error) defer close(z.errors) @@ -324,6 +345,7 @@ func (z *ZipWriter) write(out string, pathMappings []pathMapping, manifest strin case op := <-writeOpChan: currentWriteOpChan = nil + var err error if op.fh.Method == zip.Deflate { currentWriter, err = zipw.CreateCompressedHeader(op.fh) } else { @@ -356,21 +378,21 @@ func (z *ZipWriter) write(out string, pathMappings []pathMapping, manifest strin currentReader = futureReader case reader := <-currentReader: - _, err = io.Copy(currentWriter, reader) + _, err := io.Copy(currentWriter, reader) if err != nil { return err } currentReader = nil - case err = <-z.errors: + case err := <-z.errors: return err } } // One last chance to catch an error select { - case err = <-z.errors: + case err := <-z.errors: return err default: zipw.Close()