Skip to content
Merged
Show file tree
Hide file tree
Changes from 26 commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
1f8472c
Add support for zip/unzip permissions and symlinks
kiendang Mar 5, 2025
848c40a
Vendor apache ant zip
kiendang Mar 6, 2025
390ade4
Check vendored Apache Ant zip source code to git
kiendang Mar 7, 2025
075ddda
Configure Scala Steward to regenerate vendored Apache Ant source on u…
kiendang Mar 7, 2025
fed2222
Move vendored code to its own module and add shims
kiendang Mar 8, 2025
42c5a7c
Make the vendored code public
kiendang Mar 10, 2025
088bb2d
Use S_IFMT equivalent from Apache Ant
kiendang Mar 11, 2025
2bd47d6
Add tests
kiendang Mar 12, 2025
c977a95
Fix symlinks
kiendang Mar 12, 2025
ef56789
Fix Windows
kiendang Mar 12, 2025
2250210
Fix test
kiendang Mar 13, 2025
5237582
Store sym links as the referenced file by default
kiendang Mar 13, 2025
4127ca1
Replace .toNIO call with the more efficient .wrapped
kiendang Mar 16, 2025
e8a38e2
Preserve permissions for modifying a zip file in place
kiendang Mar 16, 2025
0822593
Add doc
kiendang Mar 16, 2025
86af317
Add some test comments
kiendang Mar 16, 2025
dc347a5
Rename arg
kiendang Mar 16, 2025
431e4c7
Edit doc
kiendang Mar 16, 2025
c2551e3
Add doc to mill task
kiendang Mar 16, 2025
3c6daf8
Add more test comments
kiendang Mar 17, 2025
3dfa3fd
.
kiendang Mar 17, 2025
b64a38f
Add to readme
kiendang Mar 23, 2025
f572c6d
Support zipping symlinks as symlinks on Windows
kiendang Mar 28, 2025
5725d20
Add test
kiendang Mar 28, 2025
8beed9b
Support unzipping symlinks as symlinks on Windows
kiendang Mar 28, 2025
f32ef37
Format
kiendang Mar 29, 2025
b082d41
Make the shaded code generatedSources
kiendang Apr 11, 2025
92c325a
Test zip and unzip directory permission preservation
kiendang Apr 12, 2025
580ebfe
Preserve zipped directory permissions
kiendang Apr 12, 2025
e2cce26
Test directory permission preservation for existing zips
kiendang Apr 12, 2025
00cf04e
Preserve zipped directory permissions
kiendang Apr 12, 2025
3c9e576
Add note on os.unzip.stream not supporting perms and symlinks
kiendang Apr 12, 2025
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
6 changes: 6 additions & 0 deletions .github/scala-steward.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
postUpdateHooks = [{
command = ["./mill", "os.apacheAntZipSource"],
commitMessage = "Update vendored Apache Ant",
groupId = "org.apache.ant",
artifactId = "ant"
}]
24 changes: 17 additions & 7 deletions Readme.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -1233,7 +1233,8 @@ def apply(dest: os.Path,
includePatterns: Seq[Regex] = List(),
preserveMtimes: Boolean = false,
deletePatterns: Seq[Regex] = List(),
compressionLevel: Int = -1 /* 0-9 */): os.Path
compressionLevel: Int = -1, /* 0-9 */
followLinks: Boolean = true): os.Path
----

The zip object provides functionality to create or modify zip archives. It supports:
Expand All @@ -1243,14 +1244,19 @@ The zip object provides functionality to create or modify zip archives. It suppo
- Exclude Patterns (-x): You can specify files or patterns to exclude while zipping.
- Include Patterns (-i): You can include specific files or patterns while zipping.
- Delete Patterns (-d): You can delete specific files from an existing zip archive.
- Configuring whether or not to preserve filesyste mtimes and permissions
- Symbolic Links (-y): You can configure to zip symbolic links as symbolic links on Linux/Unix by setting `followLinks = false`. Symbolic links are zipped as the referenced files by default on Linux/Unix, and always on Windows.
- Configuring whether or not to preserve filesyste mtimes.
- Preserving Unix file permissions.

This will create a new zip archive at `dest` containing `file1.txt` and everything
inside `sources`. If `dest` already exists as a zip, the files will be appended to the
existing zip, and any existing zip entries matching `deletePatterns` will be removed.

Note that `os.zip` doesn't support creating/unpacking symlinks or filesystem permissions
in Zip files, because the underlying `java.util.zip.Zip*Stream` doesn't support them.
When modifying an existing zip file,
- Unix file permissions will be preserved if Java Runtime Version >= 14.
- If using Java Runtime Version < 14, Unix file permissions are not preserved, even for existing zip entries.
- Symbolics links will always be stored as the referenced files.
- Existing symbolic links stored in the zip might lose their symbolic link file type field and become broken.

===== Zipping Files and Folders

Expand Down Expand Up @@ -1375,6 +1381,8 @@ assert(paths == Seq(unzippedFolder / "File.txt"))
This can be useful for streaming the zipped data to places which are not files:
over the network, over a pipe, etc.

File permissions will be preserved. Symbolic links will be zipped as the referenced files by default on Linux/Unix, and always on Windows. To zip them as symbolic links on Linux/Unix, set `followLinks = false`.

==== `os.unzip`

===== Unzipping Files
Expand All @@ -1384,7 +1392,7 @@ over the network, over a pipe, etc.
os.unzip(os.Path("/path/to/archive.zip"), Some(os.Path("/path/to/destination")))
----

This extracts the contents of `archive.zip` to the specified destination.
This extracts the contents of `archive.zip` to the specified destination. It supports preserving file permissions and symbolic links.


===== Excluding Files While Unzipping
Expand All @@ -1407,7 +1415,7 @@ You can list the contents of the zip file without extracting them:
os.unzip.list(os.Path("/path/to/archive.zip"))
----

This will print all the file paths contained in the zip archive.
This will print all the file paths contained in the zip archive. File permissions and symbolic links will not be preserved.

==== `os.unzip.stream`

Expand Down Expand Up @@ -1464,6 +1472,8 @@ finally zipFile3.close()
of the zip file rather than a bare path on the filesystem. Note that you need to call `ZipRoot#close()`
when you are done with it to avoid leaking filesystem resources.

File permissions are only supported for Java Runtime Version >= 14. Symbolic links are not supported. Using `os.zip.open` on a zip archive that contains symbolic links might break the links.

=== Filesystem Metadata

==== `os.stat`
Expand Down Expand Up @@ -1793,7 +1803,7 @@ is run:

* `cwd`: the working directory of the subprocess
* `env`: any additional environment variables you wish to set in the subprocess
in addition to those passed via `propagateEnv`. You can also set their values
in addition to those passed via `propagateEnv`. You can also set their values
to `null` to remove specific variables.
* `stdin`: any data you wish to pass to the subprocess's standard input
* `stdout`/`stderr`: these are ``os.Redirect``s that let you configure how the
Expand Down
59 changes: 59 additions & 0 deletions build.mill
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ object Deps {
val acyclic = ivy"com.lihaoyi:::acyclic:0.3.18"
val jna = ivy"net.java.dev.jna:jna:5.15.0"
val geny = ivy"com.lihaoyi::geny::1.1.1"
val ant = ivy"org.apache.ant:ant:1.10.15"
val sourcecode = ivy"com.lihaoyi::sourcecode::0.4.2"
val utest = ivy"com.lihaoyi::utest::0.8.4"
val expecty = ivy"com.eed3si9n.expecty::expecty::0.16.0"
Expand Down Expand Up @@ -169,6 +170,8 @@ object os extends Module {

object jvm extends Cross[OsJvmModule](scalaVersions)
trait OsJvmModule extends OsModule with MiMaChecks {
def moduleDeps = super.moduleDeps ++ Seq(os.zip.jvm())

object test extends ScalaTests with OsLibTestModule {
override def ivyDeps = T { super.ivyDeps() ++ Agg(Deps.expecty) }

Expand All @@ -194,6 +197,62 @@ object os extends Module {
object nohometest extends ScalaTests with OsLibTestModule
}

object zip extends Module {
object jvm extends Cross[OsZipJvmModule](scalaVersions)
trait OsZipJvmModule extends OsLibModule

val pkg = "os.shaded_org_apache_tools_zip"
val zipSrc = millSourcePath / "src" / "os/shaded_org_apache_tools_zip"

def apacheAntZipOriginalSource: T[PathRef] = Task(persistent = true) {
if (!_root_.os.exists(Task.dest / "unzipped")) {
val antVersion = Deps.ant.version
_root_.os.unzip.stream(
requests.get.stream(
s"https://repo1.maven.org/maven2/org/apache/ant/ant/$antVersion/ant-$antVersion-sources.jar"
),
Task.dest / "unzipped"
)
}

PathRef(Task.dest / "unzipped" / "org/apache/tools/zip")
}

/**
* Shades Apache Ant
* [[`org.apache.tools.zip` https://ant.apache.org/manual/api/org/apache/tools/zip/package-summary.html package]] to
* provide Unix file permission and symbolic link support for `os.zip` and `os.unzip`
*
* A third party dependency is needed since JDK's own
* [[`jdk.zipfs` https://docs.oracle.com/en/java/javase/14/docs/api/jdk.zipfs/module-summary.html]] does not support
* symbolic links and only supports file permissions since JDK 14.
*
* Apache Ant `org.apache.tools.zip` was chosen over Apache Commons Compress due to the former not having any
* third party dependency, only depending on Java core libraries while the later also depends on Apache Commons IO.
*
* To avoid classpath conflicts, the dependency is shaded and compiled from source. Only the `org.apache.tools.zip`
* package, not the entire Ant codebase, is needed. This only adds < 100kb to Os-Lib jar size.
*/
def apacheAntZipSource: T[PathRef] = Task {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we make this a def generatedSources folder? That way we won't won't need to commit all the vendored code to the repo, and can rely on the build tool re-generating it on demand as necessary

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes

_root_.os.walk.stream(zipSrc)
.filter(_.ext == "java")
.filter(_.last != "PermissionUtils.java")
.foreach(_root_.os.remove(_))

// Move from "package org.apache.tools.zip" to "package os.shaded_org_apache_tools_zip"
// Make all classes package private (private [os]) by removing any `public` access modifier
_root_.os.walk.stream(apacheAntZipOriginalSource().path)
.filter(_.ext == "java")
.foreach { p =>
val content = _root_.os.read(p)
.replaceAll("org.apache.tools.zip", pkg)
_root_.os.write(zipSrc / p.last, content)
}

PathRef(zipSrc)
}
}

/*object native extends Cross[OsNativeModule](scalaVersions)
trait OsNativeModule extends OsModule with ScalaNativeModule {
def scalaNativeVersion = "0.5.2"
Expand Down
Loading