├── .git-blame-ignore-revs ├── .github ├── dependabot.yml └── workflows │ ├── ci.yml │ └── cla.yml ├── .gitignore ├── .java-version ├── .scalafmt.conf ├── LICENSE ├── NOTICE ├── build.sbt ├── io └── src │ ├── main │ ├── contraband-scala │ │ └── sbt │ │ │ └── io │ │ │ └── CopyOptions.scala │ ├── contraband │ │ └── io.contra │ └── scala │ │ └── sbt │ │ ├── internal │ │ ├── io │ │ │ ├── DeferredWriter.scala │ │ │ ├── ErrorHandling.scala │ │ │ ├── MacOSXWatchService.scala │ │ │ ├── Milli.scala │ │ │ ├── Resources.scala │ │ │ ├── Retry.scala │ │ │ └── SourceModificationWatch.scala │ │ └── nio │ │ │ ├── FileCache.scala │ │ │ ├── FileEvent.scala │ │ │ ├── FileEventMonitor.scala │ │ │ ├── FileTreeRepository.scala │ │ │ ├── FileTreeRepositoryImpl.scala │ │ │ ├── Globs.scala │ │ │ ├── LegacyFileTreeRepository.scala │ │ │ ├── Observers.scala │ │ │ ├── PollingWatchService.scala │ │ │ ├── SwovalConverters.scala │ │ │ ├── TimeSource.scala │ │ │ ├── WatchLogger.scala │ │ │ └── WatchServiceBackedObservable.scala │ │ ├── io │ │ ├── Hash.scala │ │ ├── IO.scala │ │ ├── JavaMilli.scala │ │ ├── NameFilter.scala │ │ ├── Path.scala │ │ ├── PathMapper.scala │ │ ├── Using.scala │ │ ├── WatchService.scala │ │ └── syntax.scala │ │ └── nio │ │ └── file │ │ ├── ChangedFiles.scala │ │ ├── FileAttributes.scala │ │ ├── FileTreeView.scala │ │ ├── Glob.scala │ │ ├── PathFilter.scala │ │ ├── package.scala │ │ └── syntax │ │ └── package.scala │ └── test │ ├── resources │ └── zip-slip.zip │ └── scala │ └── sbt │ ├── internal │ ├── io │ │ ├── DefaultWatchServiceSpec.scala │ │ ├── JavaMilliSpec.scala │ │ ├── PollingWatchServiceSpec.scala │ │ ├── RetrySpec.scala │ │ ├── SourceModificationWatchSpec.scala │ │ ├── SourceSpec.scala │ │ └── WatchServiceBackedObservableSpec.scala │ └── nio │ │ ├── FileEventMonitorSpec.scala │ │ ├── FileTreeRepositorySpec.scala │ │ ├── GlobsSpec.scala │ │ ├── MacOSXWatchServiceSpec.scala │ │ └── PathSyntaxSpec.scala │ ├── io │ ├── CombinedFilterSpec.scala │ ├── CopyDirectorySpec.scala │ ├── CopySpec.scala │ ├── FileSpec.scala │ ├── FileUtilitiesSpecification.scala │ ├── GlobFilterSpec.scala │ ├── IOSpec.scala │ ├── IOSpecification.scala │ ├── IOSyntaxSpec.scala │ ├── LastModifiedSpec.scala │ ├── NameFilterSpec.scala │ ├── NameFilterSpecification.scala │ ├── PathFinderCombinatorSpec.scala │ ├── PathFinderSpec.scala │ ├── PathMapperSpec.scala │ ├── StashSpec.scala │ └── WithFiles.scala │ └── nio │ ├── FileAttributeSpec.scala │ ├── FileTreeViewSpec.scala │ ├── GlobFilterSpec.scala │ ├── GlobOrderingSpec.scala │ ├── GlobParserSpec.scala │ ├── GlobSyntaxSpec.scala │ ├── PathFilterSpec.scala │ ├── TestHelpers.scala │ └── TraversableGlobSpec.scala ├── notes └── 1.0.0.markdown └── project ├── Dependencies.scala ├── HouseRulesPluglin.scala ├── build.properties └── plugins.sbt /.git-blame-ignore-revs: -------------------------------------------------------------------------------- 1 | # Scala Steward: Reformat with scalafmt 3.7.17 2 | 51ff6ed032bdebd73d089cc9ecf8f062b53bc07d 3 | 4 | # Scala Steward: Reformat with scalafmt 3.9.9 5 | 3cb284e363b44e32f9fafde76de2f9a1e7a87390 6 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | schedule: 6 | interval: "weekly" 7 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | pull_request: 4 | push: 5 | 6 | env: 7 | SCALA212: 2.12.* 8 | SCALA213: 2.13.* 9 | SCALA3: 3.* 10 | 11 | jobs: 12 | test: 13 | strategy: 14 | fail-fast: false 15 | matrix: 16 | include: 17 | - os: ubuntu-latest 18 | java: 8 19 | distribution: zulu 20 | jobtype: 1 21 | - os: ubuntu-latest 22 | java: 11 23 | distribution: temurin 24 | jobtype: 2 25 | - os: ubuntu-latest 26 | java: 17 27 | distribution: temurin 28 | jobtype: 2 29 | - os: ubuntu-latest 30 | java: 21 31 | distribution: temurin 32 | jobtype: 2 33 | - os: macos-latest 34 | java: 11 35 | distribution: temurin 36 | jobtype: 2 37 | - os: windows-2025 38 | java: 8 39 | distribution: zulu 40 | jobtype: 3 41 | runs-on: ${{ matrix.os }} 42 | env: 43 | JAVA_OPTS: -Dsbt.io.virtual=false -Dfile.encoding=UTF-8 44 | JVM_OPTS: -Dsbt.io.virtual=false -Dfile.encoding=UTF-8 45 | steps: 46 | - name: Checkout 47 | uses: actions/checkout@v6 48 | - name: Setup JDK 49 | uses: actions/setup-java@v5 50 | with: 51 | distribution: "${{ matrix.distribution }}" 52 | java-version: "${{ matrix.java }}" 53 | cache: sbt 54 | - name: Setup sbt 55 | uses: sbt/setup-sbt@v1 56 | - name: Build and test (1) 57 | if: ${{ matrix.jobtype == 1 }} 58 | shell: bash 59 | run: | 60 | find **/src/main/contraband-scala -name "*.scala" -delete 61 | sbt -v -Dfile.encoding=UTF8 -Dsbt.test.fork=true ++$SCALA212 generateContrabands mimaReportBinaryIssues scalafmtCheckAll headerCheck Test/headerCheck test doc 62 | sbt -v -Dfile.encoding=UTF8 -Dsbt.test.fork=false ++$SCALA213 test ++$SCALA3 test 63 | git diff --exit-code # check contrabands 64 | - name: Build and test (2) 65 | if: ${{ matrix.jobtype == 2 }} 66 | shell: bash 67 | run: | 68 | sbt -v -Dfile.encoding=UTF8 -Dsbt.test.fork=true ++$SCALA212 mimaReportBinaryIssues test 69 | sbt -v --client ++$SCALA213 70 | sbt -v --client test 71 | sbt -v --client ++$SCALA3 72 | sbt -v --client test 73 | - name: Build and test (3) 74 | if: ${{ matrix.jobtype == 3 }} 75 | shell: bash 76 | run: | 77 | sbt -v -Dfile.encoding=UTF8 -Dsbt.test.fork=true ++$SCALA212 test 78 | sbt -v --client ++$SCALA213 79 | sbt -v --client test 80 | sbt -v --client ++$SCALA3 81 | sbt -v --client test 82 | -------------------------------------------------------------------------------- /.github/workflows/cla.yml: -------------------------------------------------------------------------------- 1 | name: Scala CLA 2 | on: [pull_request] 3 | jobs: 4 | check: 5 | runs-on: ubuntu-latest 6 | steps: 7 | - name: Check CLA 8 | uses: scala/cla-checker@v1 9 | with: 10 | author: ${{ github.event.pull_request.user.login }} 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | -------------------------------------------------------------------------------- /.java-version: -------------------------------------------------------------------------------- 1 | 1.8 2 | -------------------------------------------------------------------------------- /.scalafmt.conf: -------------------------------------------------------------------------------- 1 | version = 3.10.2 2 | maxColumn = 100 3 | project.git = true 4 | project.excludeFilters = [ /sbt-test/, /input_sources/, /contraband-scala/ ] 5 | 6 | # http://docs.scala-lang.org/style/scaladoc.html recommends the JavaDoc style. 7 | # scala/scala is written that way too https://github.com/scala/scala/blob/v2.12.2/src/library/scala/Predef.scala 8 | docstrings.style = Asterisk 9 | docstrings.wrap = no 10 | 11 | # This also seems more idiomatic to include whitespace in import x.{ yyy } 12 | spaces.inImportCurlyBraces = true 13 | 14 | # This is more idiomatic Scala. 15 | # http://docs.scala-lang.org/style/indentation.html#methods-with-numerous-arguments 16 | align.openParenCallSite = false 17 | align.openParenDefnSite = false 18 | 19 | # For better code clarity 20 | danglingParentheses.preset = true 21 | 22 | trailingCommas = preserve 23 | 24 | runner.dialect = Scala212Source3 25 | 26 | rewrite.scala3.convertToNewSyntax = true 27 | runner.dialectOverride.allowSignificantIndentation = false 28 | runner.dialectOverride.allowAsForImportRename = false 29 | runner.dialectOverride.allowStarWildcardImport = false 30 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | sbt: I/O Component 2 | Copyright 2023, Scala Center 3 | Copyright 2008-2022 Lightbend, Inc, Mark Harrah, Eugene Yokota, Josh Suereth, Antonio Cunei, Viktor Klang, Ross McDonald, and other contributors 4 | Licensed under Apache 2.0 license (see LICENSE) 5 | -------------------------------------------------------------------------------- /build.sbt: -------------------------------------------------------------------------------- 1 | import Dependencies._ 2 | import com.typesafe.tools.mima.core._, ProblemFilters._ 3 | 4 | ThisBuild / version := { 5 | val old = (ThisBuild / version).value 6 | (sys.env.get("BUILD_VERSION") orElse sys.props.get("sbt.build.version")) match { 7 | case Some(v) => v 8 | case _ => 9 | if ((ThisBuild / isSnapshot).value) "1.6.0-SNAPSHOT" 10 | else old 11 | } 12 | } 13 | ThisBuild / versionScheme := Some("early-semver") 14 | ThisBuild / organization := "org.scala-sbt" 15 | ThisBuild / homepage := Some(url("https://github.com/sbt/io")) 16 | ThisBuild / description := "IO module for sbt" 17 | ThisBuild / scmInfo := Some(ScmInfo(url("https://github.com/sbt/io"), "git@github.com:sbt/io.git")) 18 | ThisBuild / licenses := List(("Apache-2.0", url("https://www.apache.org/licenses/LICENSE-2.0"))) 19 | ThisBuild / headerLicense := Some( 20 | HeaderLicense.Custom( 21 | s"""sbt IO 22 | |Copyright Scala Center, Lightbend, and Mark Harrah 23 | | 24 | |Licensed under Apache License 2.0 25 | |SPDX-License-Identifier: Apache-2.0 26 | | 27 | |See the NOTICE file distributed with this work for 28 | |additional information regarding copyright ownership. 29 | |""".stripMargin 30 | ) 31 | ) 32 | ThisBuild / scalafmtOnCompile := true 33 | ThisBuild / developers := List( 34 | Developer("eatkins", "Ethan Atkins", "@eatkins", url("https://www.ethanatkins.com/")), 35 | Developer("harrah", "Mark Harrah", "@harrah", url("https://github.com/harrah")), 36 | Developer("eed3si9n", "Eugene Yokota", "@eed3si9n", url("http://eed3si9n.com/")), 37 | Developer("dwijnand", "Dale Wijnand", "@dwijnand", url("https://github.com/dwijnand")), 38 | ) 39 | ThisBuild / turbo := true 40 | ThisBuild / pomIncludeRepository := (_ => false) 41 | ThisBuild / publishTo := { 42 | val centralSnapshots = "https://central.sonatype.com/repository/maven-snapshots/" 43 | if (isSnapshot.value) Some("central-snapshots" at centralSnapshots) 44 | else localStaging.value 45 | } 46 | 47 | def commonSettings: Seq[Setting[?]] = Seq( 48 | scalaVersion := scala212, 49 | compile / javacOptions ++= Seq("-Xlint", "-Xlint:-serial"), 50 | crossScalaVersions := Seq(scala212, scala213, scala3), 51 | headerLicense := (ThisBuild / headerLicense).value, 52 | ) 53 | 54 | lazy val ioRoot = (project in file(".")) 55 | .aggregate(io) 56 | .settings( 57 | commonSettings, 58 | name := "IO Root", 59 | publish / skip := true, 60 | onLoadMessage := { 61 | """ _ 62 | | (_)___ 63 | | / / __ \ 64 | | / / /_/ / 65 | | /_/\____/ 66 | |Welcome to the build for sbt/io. 67 | |""".stripMargin + 68 | (if (sys.props("java.specification.version") != "1.8") 69 | s"""!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! 70 | | Java versions is ${sys.props("java.specification.version")}. We recommend 1.8. 71 | |!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!""".stripMargin 72 | else "") 73 | }, 74 | mimaPreviousArtifacts := Set.empty, 75 | ) 76 | 77 | // Path, IO (formerly FileUtilities), NameFilter and other I/O utility classes 78 | val io = (project in file("io")) 79 | .enablePlugins(ContrabandPlugin) 80 | .settings( 81 | commonSettings, 82 | name := "IO", 83 | libraryDependencies ++= { 84 | Vector(scalaCompiler.value % Test, scalaVerify % Test, scalaCheck % Test, scalatest % Test) 85 | } ++ Vector(swovalFiles), 86 | testFrameworks += new TestFramework("verify.runner.Framework"), 87 | Test / fork := System.getProperty("sbt.test.fork", "false") == "true", 88 | Test / testForkedParallel := true, 89 | Compile / generateContrabands / sourceManaged := baseDirectory.value / "src" / "main" / "contraband-scala", 90 | console / initialCommands += "\nimport sbt.io._, syntax._", 91 | mimaPreviousArtifacts := (CrossVersion partialVersion scalaVersion.value match { 92 | case Some((2, n)) if n >= 13 => Set.empty 93 | case _ => 94 | Set( 95 | "1.0.0", 96 | "1.0.1", 97 | "1.0.2", 98 | "1.1.0", 99 | "1.1.1", 100 | "1.1.2", 101 | "1.1.3", 102 | "1.1.4", 103 | "1.2.0", 104 | "1.3.0", 105 | "1.4.0", 106 | "1.5.0", 107 | "1.6.0", 108 | "1.7.0", 109 | "1.8.0", 110 | "1.9.0", 111 | "1.10.0", 112 | ) map (version => organization.value %% moduleName.value % version) 113 | }), 114 | mimaBinaryIssueFilters ++= Seq( 115 | exclude[FinalClassProblem]("sbt.internal.io.MacJNA$TimeBuf"), 116 | // MiMa doesn't treat effectively final members as final 117 | // WORKAROUND typesafehub/migration-manager#162 118 | exclude[FinalMethodProblem]("sbt.io.SimpleFilter.accept"), 119 | exclude[FinalMethodProblem]("sbt.io.SimpleFileFilter.accept"), 120 | // MiMa doesn't understand private inner classes? 121 | // method this(sbt.io.PollingWatchService,sbt.io.PollingWatchService#PollingThread,java.nio.file.Watchable,java.util.List)Unit in class sbt.io.PollingWatchService#PollingWatchKey does not have a correspondent in current version 122 | exclude[DirectMissingMethodProblem]("sbt.io.PollingWatchService#PollingWatchKey.this"), 123 | exclude[IncompatibleMethTypeProblem]("sbt.io.PollingWatchService#PollingWatchKey.this"), 124 | // This is a private class 125 | exclude[DirectMissingMethodProblem]("sbt.io.PollingWatchService#PollingWatchKey.events"), 126 | exclude[DirectMissingMethodProblem]("sbt.io.PollingWatchService#PollingWatchKey.offer"), 127 | // This is a private class 128 | exclude[DirectMissingMethodProblem]("sbt.io.PollingWatchService#PollingThread.events"), 129 | exclude[DirectMissingMethodProblem]("sbt.io.PollingWatchService#PollingThread.initDone"), 130 | exclude[DirectMissingMethodProblem]("sbt.io.PollingWatchService#PollingThread.initDone_="), 131 | exclude[DirectMissingMethodProblem]( 132 | "sbt.io.PollingWatchService#PollingThread.keysWithEvents" 133 | ), 134 | exclude[DirectMissingMethodProblem]("sbt.io.PollingWatchService#PollingThread.getFileTimes"), 135 | // moved JavaMilli to sbt.io 136 | exclude[MissingClassProblem]("sbt.internal.io.JavaMilli$"), 137 | exclude[MissingClassProblem]("sbt.internal.io.JavaMilli"), 138 | // These are private classes 139 | exclude[MissingClassProblem]("sbt.internal.io.*"), 140 | // Replaced non-standard __xstat64() with conformant stat() calls 141 | exclude[DirectMissingMethodProblem]("sbt.internal.io.Linux32.*"), 142 | exclude[ReversedMissingMethodProblem]("sbt.internal.io.Linux32.*"), 143 | exclude[DirectMissingMethodProblem]("sbt.internal.io.Linux64.*"), 144 | exclude[ReversedMissingMethodProblem]("sbt.internal.io.Linux64.*"), 145 | // protected[this] 146 | exclude[DirectMissingMethodProblem]("sbt.io.CopyOptions.copy*"), 147 | // private class 148 | exclude[MissingClassProblem]("sbt.io.Event"), 149 | exclude[MissingClassProblem]("sbt.io.Event$"), 150 | exclude[MissingClassProblem]("sbt.io.MacOSXWatchKey"), 151 | exclude[MissingClassProblem]("sbt.io.PollingWatchEvent"), 152 | exclude[MissingClassProblem]("sbt.io.PollingWatchService$PollingWatchKey"), 153 | exclude[MissingClassProblem]("sbt.io.PollingWatchService$PollingThread"), 154 | exclude[MissingClassProblem]("sbt.io.PollingWatchService$Overflow$"), 155 | // private internal classes whose functionality has been replaced 156 | exclude[MissingClassProblem]("sbt.internal.io.EventMonitor*"), 157 | exclude[DirectMissingMethodProblem]("sbt.internal.io.EventMonitor.legacy"), 158 | exclude[DirectMissingMethodProblem]("sbt.internal.io.EventMonitor.applyImpl"), 159 | // private classes that have been removed 160 | exclude[MissingClassProblem]("sbt.internal.io.Alternatives$"), 161 | exclude[MissingClassProblem]("sbt.internal.io.Alternatives"), 162 | exclude[DirectMissingMethodProblem]("sbt.io.NothingFilter.unary_-"), 163 | exclude[DirectMissingMethodProblem]("sbt.io.AllPassFilter.unary_-"), 164 | exclude[IncompatibleSignatureProblem]("sbt.io.PollingWatchService.pollEvents"), 165 | exclude[IncompatibleSignatureProblem]("sbt.io.WatchService#WatchServiceAdapter.pollEvents"), 166 | exclude[IncompatibleSignatureProblem]("sbt.io.WatchService.pollEvents"), 167 | ), 168 | BuildInfoPlugin.buildInfoDefaultSettings, // avoids BuildInfo generated in Compile scope 169 | addBuildInfoToConfig(Test), 170 | Test / buildInfoKeys := Seq[BuildInfoKey](target), 171 | Test / buildInfoPackage := "sbt.internal.io", 172 | Test / buildInfoUsePackageAsPath := true, 173 | ) 174 | -------------------------------------------------------------------------------- /io/src/main/contraband-scala/sbt/io/CopyOptions.scala: -------------------------------------------------------------------------------- 1 | /** 2 | * This code is generated using [[https://www.scala-sbt.org/contraband]]. 3 | */ 4 | 5 | // DO NOT EDIT MANUALLY 6 | package sbt.io 7 | /** 8 | * The options for the copy operation in `IO`. 9 | * @param overwrite A source file is always copied if `overwrite` is true. 10 | If `overwrite` is false, the source is only copied if the target is missing or is older than the 11 | source file according to last modified times. 12 | If the source is a directory, the corresponding directory is created. 13 | * @param preserveLastModified If `true` the last modified times are copied. 14 | * @param preserveExecutable If `true` the executable properties are copied. 15 | */ 16 | final class CopyOptions private ( 17 | val overwrite: Boolean, 18 | val preserveLastModified: Boolean, 19 | val preserveExecutable: Boolean) extends Serializable { 20 | 21 | private def this() = this(false, false, true) 22 | 23 | override def equals(o: Any): Boolean = this.eq(o.asInstanceOf[AnyRef]) || (o match { 24 | case x: CopyOptions => (this.overwrite == x.overwrite) && (this.preserveLastModified == x.preserveLastModified) && (this.preserveExecutable == x.preserveExecutable) 25 | case _ => false 26 | }) 27 | override def hashCode: Int = { 28 | 37 * (37 * (37 * (37 * (17 + "sbt.io.CopyOptions".##) + overwrite.##) + preserveLastModified.##) + preserveExecutable.##) 29 | } 30 | override def toString: String = { 31 | "CopyOptions(" + overwrite + ", " + preserveLastModified + ", " + preserveExecutable + ")" 32 | } 33 | private def copy(overwrite: Boolean = overwrite, preserveLastModified: Boolean = preserveLastModified, preserveExecutable: Boolean = preserveExecutable): CopyOptions = { 34 | new CopyOptions(overwrite, preserveLastModified, preserveExecutable) 35 | } 36 | def withOverwrite(overwrite: Boolean): CopyOptions = { 37 | copy(overwrite = overwrite) 38 | } 39 | def withPreserveLastModified(preserveLastModified: Boolean): CopyOptions = { 40 | copy(preserveLastModified = preserveLastModified) 41 | } 42 | def withPreserveExecutable(preserveExecutable: Boolean): CopyOptions = { 43 | copy(preserveExecutable = preserveExecutable) 44 | } 45 | } 46 | object CopyOptions { 47 | 48 | def apply(): CopyOptions = new CopyOptions() 49 | def apply(overwrite: Boolean, preserveLastModified: Boolean, preserveExecutable: Boolean): CopyOptions = new CopyOptions(overwrite, preserveLastModified, preserveExecutable) 50 | } 51 | -------------------------------------------------------------------------------- /io/src/main/contraband/io.contra: -------------------------------------------------------------------------------- 1 | package sbt.io 2 | @target(Scala) 3 | 4 | ## The options for the copy operation in `IO`. 5 | type CopyOptions { 6 | 7 | ## A source file is always copied if `overwrite` is true. 8 | ## If `overwrite` is false, the source is only copied if the target is missing or is older than the 9 | ## source file according to last modified times. 10 | ## If the source is a directory, the corresponding directory is created. 11 | overwrite: boolean! = false @since("0.0.1") 12 | 13 | ## If `true` the last modified times are copied. 14 | preserveLastModified: boolean! = false @since("0.0.1") 15 | 16 | ## If `true` the executable properties are copied. 17 | preserveExecutable: boolean! = true @since("0.0.1") 18 | 19 | } 20 | -------------------------------------------------------------------------------- /io/src/main/scala/sbt/internal/io/DeferredWriter.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * sbt IO 3 | * Copyright Scala Center, Lightbend, and Mark Harrah 4 | * 5 | * Licensed under Apache License 2.0 6 | * SPDX-License-Identifier: Apache-2.0 7 | * 8 | * See the NOTICE file distributed with this work for 9 | * additional information regarding copyright ownership. 10 | */ 11 | 12 | package sbt.internal.io 13 | 14 | import java.io.Writer 15 | 16 | /** A `Writer` that avoids constructing the underlying `Writer` with `make` until a method other than `close` is called on this `Writer`. */ 17 | private[sbt] final class DeferredWriter(make: => Writer) extends Writer { 18 | private[this] var opened = false 19 | private[this] var delegate0: Writer = _ 20 | private[this] def delegate: Writer = synchronized { 21 | if (delegate0 eq null) { 22 | delegate0 = make 23 | opened = true 24 | } 25 | delegate0 26 | } 27 | override def close() = synchronized { 28 | if (opened) delegate0.close() 29 | } 30 | 31 | override def append(c: Char): Writer = delegate.append(c) 32 | override def append(csq: CharSequence): Writer = delegate.append(csq) 33 | 34 | override def append(csq: CharSequence, start: Int, end: Int): Writer = 35 | delegate.append(csq, start, end) 36 | 37 | override def flush() = delegate.flush() 38 | override def write(cbuf: Array[Char]) = delegate.write(cbuf) 39 | override def write(cbuf: Array[Char], off: Int, len: Int): Unit = delegate.write(cbuf, off, len) 40 | override def write(c: Int): Unit = delegate.write(c) 41 | override def write(s: String): Unit = delegate.write(s) 42 | override def write(s: String, off: Int, len: Int): Unit = delegate.write(s, off, len) 43 | } 44 | -------------------------------------------------------------------------------- /io/src/main/scala/sbt/internal/io/ErrorHandling.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * sbt IO 3 | * Copyright Scala Center, Lightbend, and Mark Harrah 4 | * 5 | * Licensed under Apache License 2.0 6 | * SPDX-License-Identifier: Apache-2.0 7 | * 8 | * See the NOTICE file distributed with this work for 9 | * additional information regarding copyright ownership. 10 | */ 11 | 12 | package sbt.internal.io 13 | 14 | import java.io.IOException 15 | 16 | private[sbt] object ErrorHandling { 17 | def translate[T](msg: => String)(f: => T) = 18 | try { 19 | f 20 | } catch { 21 | case e: IOException => throw new TranslatedIOException(msg + e.toString, e) 22 | case e: Exception => throw new TranslatedException(msg + e.toString, e) 23 | } 24 | 25 | def wideConvert[T](f: => T): Either[Throwable, T] = 26 | try { 27 | Right(f) 28 | } catch { 29 | case ex @ (_: Exception | _: StackOverflowError) => Left(ex) 30 | case err @ (_: ThreadDeath | _: VirtualMachineError) => throw err 31 | case x: Throwable => Left(x) 32 | } 33 | 34 | def convert[T](f: => T): Either[Exception, T] = 35 | try { 36 | Right(f) 37 | } catch { case e: Exception => Left(e) } 38 | 39 | def reducedToString(e: Throwable): String = 40 | if (e.getClass == classOf[RuntimeException]) { 41 | val msg = e.getMessage 42 | if (msg == null || msg.isEmpty) e.toString else msg 43 | } else 44 | e.toString 45 | } 46 | 47 | private[sbt] sealed class TranslatedException private[sbt] (msg: String, cause: Throwable) 48 | extends RuntimeException(msg, cause) { 49 | override def toString = msg 50 | } 51 | 52 | private[sbt] final class TranslatedIOException private[sbt] (msg: String, cause: IOException) 53 | extends TranslatedException(msg, cause) 54 | -------------------------------------------------------------------------------- /io/src/main/scala/sbt/internal/io/MacOSXWatchService.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * sbt IO 3 | * Copyright Scala Center, Lightbend, and Mark Harrah 4 | * 5 | * Licensed under Apache License 2.0 6 | * SPDX-License-Identifier: Apache-2.0 7 | * 8 | * See the NOTICE file distributed with this work for 9 | * additional information regarding copyright ownership. 10 | */ 11 | 12 | package sbt.internal.io 13 | 14 | import java.io.IOException 15 | import java.nio.file.{ ClosedWatchServiceException, WatchEvent, WatchKey, Path => JPath } 16 | import java.util.Collections 17 | import java.util.concurrent.atomic.AtomicBoolean 18 | import java.util.concurrent.{ ConcurrentHashMap, TimeUnit } 19 | 20 | import sbt.io.{ Unregisterable, WatchService } 21 | 22 | import scala.annotation.tailrec 23 | import scala.collection.JavaConverters._ 24 | import scala.collection.{ immutable, mutable } 25 | import scala.concurrent.duration._ 26 | 27 | private[sbt] class MacOSXWatchService extends WatchService with Unregisterable { 28 | private val underlying = com.swoval.files.RegisterableWatchServices.get() 29 | private val keys: mutable.Map[JPath, WatchKey] = 30 | Collections.synchronizedMap(new ConcurrentHashMap[JPath, WatchKey]()).asScala 31 | private val parentKeys: mutable.Map[JPath, WatchKey] = 32 | Collections.synchronizedMap(new ConcurrentHashMap[JPath, WatchKey]()).asScala 33 | private val isClosed = new AtomicBoolean(false) 34 | def isOpen: Boolean = !isClosed.get 35 | 36 | override def init(): Unit = {} 37 | 38 | override def pollEvents(): Map[WatchKey, immutable.Seq[WatchEvent[JPath]]] = { 39 | underlying.poll() match { 40 | case null => Map.empty 41 | case k => 42 | k.watchable() match { 43 | case p: JPath if keys.contains(p) => 44 | val res = k -> k 45 | .pollEvents() 46 | .asScala 47 | .view 48 | .map(_.asInstanceOf[WatchEvent[JPath]]) 49 | .toIndexedSeq 50 | Map(res) 51 | case _ => null 52 | } 53 | } 54 | } 55 | 56 | override def poll(timeout: Duration): WatchKey = { 57 | val finiteDuration: FiniteDuration = timeout match { 58 | case d: FiniteDuration => d 59 | case _ => new FiniteDuration(Int.MaxValue, SECONDS) 60 | } 61 | val limit = finiteDuration.fromNow 62 | @tailrec def impl(): WatchKey = { 63 | val remaining = limit - Deadline.now 64 | if (remaining > 0.seconds) { 65 | underlying.poll((limit - Deadline.now).toNanos, TimeUnit.NANOSECONDS) match { 66 | case null => null 67 | case k => 68 | k.watchable match { 69 | case p: JPath if keys.contains(p) => k 70 | case _ => impl() 71 | } 72 | } 73 | } else null 74 | } 75 | impl() 76 | } 77 | 78 | override def register(path: JPath, events: WatchEvent.Kind[JPath]*): WatchKey = 79 | if (!isClosed.get()) { 80 | keys.get(path) match { 81 | case Some(k) => k 82 | case _ => 83 | val resolved = resolve(path) 84 | val parent = path.getParent 85 | if (!keys.contains(parent)) { 86 | // workaround for https://github.com/sbt/sbt/issues/4603 87 | if ( 88 | keys.keys.exists(p => 89 | p.getParent == parent && { 90 | val leftFileName = p.getFileName.toString 91 | val rightFileName = path.getFileName.toString 92 | leftFileName != rightFileName && (leftFileName 93 | .startsWith(rightFileName) || rightFileName.startsWith(leftFileName)) 94 | } 95 | ) 96 | ) { 97 | parentKeys.put(parent, underlying.register(parent, events*)) 98 | } 99 | } 100 | val key = 101 | parentKeys.remove(resolved) match { 102 | case Some(k) => k 103 | case _ => 104 | underlying.register(resolved, events*) 105 | } 106 | keys.put(resolved, key) 107 | key 108 | } 109 | } else { 110 | throw new ClosedWatchServiceException 111 | } 112 | 113 | override def unregister(path: JPath): Unit = { 114 | keys.remove(resolve(path)) foreach (_.cancel()) 115 | } 116 | 117 | override def close(): Unit = if (isClosed.compareAndSet(false, true)) { 118 | keys.values.foreach(_.cancel()) 119 | keys.clear() 120 | underlying.close() 121 | } 122 | 123 | private def resolve(path: JPath): JPath = 124 | try path.toRealPath() 125 | catch { case _: IOException => if (path.isAbsolute) path else path.toAbsolutePath } 126 | } 127 | -------------------------------------------------------------------------------- /io/src/main/scala/sbt/internal/io/Milli.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * sbt IO 3 | * Copyright Scala Center, Lightbend, and Mark Harrah 4 | * 5 | * Licensed under Apache License 2.0 6 | * SPDX-License-Identifier: Apache-2.0 7 | * 8 | * See the NOTICE file distributed with this work for 9 | * additional information regarding copyright ownership. 10 | */ 11 | 12 | package sbt.internal.io 13 | 14 | import java.io.File 15 | import sbt.io.JavaMilli 16 | 17 | private[sbt] abstract class Milli { 18 | def getModifiedTime(filePath: String): Long 19 | def setModifiedTime(filePath: String, mtime: Long): Unit 20 | def copyModifiedTime(fromFilePath: String, toFilePath: String): Unit 21 | } 22 | 23 | // No native time information? Copy just the milliseconds 24 | private[sbt] abstract class MilliMilliseconds extends Milli { 25 | def copyModifiedTime(fromFilePath: String, toFilePath: String): Unit = 26 | setModifiedTime(toFilePath, getModifiedTime(fromFilePath)) 27 | } 28 | 29 | object Milli { 30 | val milli: Milli = JavaMilli 31 | def getModifiedTime(file: File): Long = 32 | milli.getModifiedTime(file.getPath) 33 | def setModifiedTime(file: File, mtime: Long): Unit = 34 | milli.setModifiedTime(file.getPath, mtime) 35 | def copyModifiedTime(fromFile: File, toFile: File): Unit = 36 | milli.copyModifiedTime(fromFile.getPath, toFile.getPath) 37 | def getMilliSupportDiagnostic(projectDir: File): Option[String] = None 38 | } 39 | -------------------------------------------------------------------------------- /io/src/main/scala/sbt/internal/io/Resources.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * sbt IO 3 | * Copyright Scala Center, Lightbend, and Mark Harrah 4 | * 5 | * Licensed under Apache License 2.0 6 | * SPDX-License-Identifier: Apache-2.0 7 | * 8 | * See the NOTICE file distributed with this work for 9 | * additional information regarding copyright ownership. 10 | */ 11 | 12 | package sbt.internal.io 13 | 14 | import java.io.File 15 | 16 | import sbt.io.IO 17 | 18 | private[sbt] object Resources { 19 | def apply(basePath: String) = { 20 | require(basePath.startsWith("/")) 21 | val resource = getClass.getResource(basePath) 22 | if (resource == null) 23 | error("Resource base directory '" + basePath + "' not on classpath.") 24 | else { 25 | val file = IO.toFile(resource) 26 | if (file.exists) 27 | new Resources(file) 28 | else 29 | error("Resource base directory '" + basePath + "' does not exist.") 30 | } 31 | } 32 | def error(msg: String) = throw new ResourcesException(msg) 33 | } 34 | private[sbt] final class ResourcesException(msg: String) extends Exception(msg) 35 | 36 | private[sbt] final class Resources(val baseDirectory: File) { 37 | import Resources._ 38 | // The returned directory is not actually read-only, but it should be treated that way 39 | def readOnlyResourceDirectory(group: String, name: String): File = { 40 | val groupDirectory = new File(baseDirectory, group) 41 | if (groupDirectory.isDirectory) { 42 | val resourceDirectory = new File(groupDirectory, name) 43 | if (resourceDirectory.isDirectory) 44 | resourceDirectory 45 | else 46 | error("Resource directory '" + name + "' in group '" + group + "' not found.") 47 | } else 48 | error("Group '" + group + "' not found.") 49 | } 50 | def readWriteResourceDirectory[T](group: String, name: String)(withDirectory: File => T): T = { 51 | val file = readOnlyResourceDirectory(group, name) 52 | readWriteResourceDirectory(file)(withDirectory) 53 | } 54 | 55 | def readWriteResourceDirectory[T](readOnly: File)(withDirectory: File => T): T = { 56 | require(readOnly.isDirectory) 57 | def readWrite(readOnly: File)(temporary: File): T = { 58 | val readWriteDirectory = new File(temporary, readOnly.getName) 59 | IO.copyDirectory(readOnly, readWriteDirectory) 60 | withDirectory(readWriteDirectory) 61 | } 62 | IO.withTemporaryDirectory(readWrite(readOnly)) 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /io/src/main/scala/sbt/internal/io/Retry.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * sbt IO 3 | * Copyright Scala Center, Lightbend, and Mark Harrah 4 | * 5 | * Licensed under Apache License 2.0 6 | * SPDX-License-Identifier: Apache-2.0 7 | * 8 | * See the NOTICE file distributed with this work for 9 | * additional information regarding copyright ownership. 10 | */ 11 | 12 | package sbt.internal.io 13 | import java.io.IOException 14 | 15 | import scala.util.control.NonFatal 16 | 17 | private[sbt] object Retry { 18 | private lazy val limit = { 19 | val defaultLimit = 10 20 | try System.getProperty("sbt.io.retry.limit", defaultLimit.toString).toInt 21 | catch { case NonFatal(_) => defaultLimit } 22 | } 23 | private final val defaultSleepInMillis = 100 24 | 25 | /** 26 | * Retry on all non-fatal exceptions that are NOT listed in 27 | * the excludedExceptions list. 28 | */ 29 | private[sbt] def apply[@specialized T](f: => T, excludedExceptions: Class[? <: Throwable]*): T = 30 | apply(f, limit, excludedExceptions*) 31 | 32 | /** 33 | * Retry on all non-fatal exceptions that are NOT listed in 34 | * the excludedExceptions list. 35 | */ 36 | private[sbt] def apply[@specialized T]( 37 | f: => T, 38 | limit: Int, 39 | excludedExceptions: Class[? <: Throwable]*, 40 | ): T = apply(f, limit, defaultSleepInMillis, excludedExceptions*) 41 | 42 | /** 43 | * Retry on all non-fatal exceptions that are NOT listed in 44 | * the excludedExceptions list. 45 | */ 46 | private[sbt] def apply[@specialized T]( 47 | f: => T, 48 | limit: Int, 49 | sleepInMillis: Long, 50 | excludedExceptions: Class[? <: Throwable]*, 51 | ): T = { 52 | def allowRetry(e: Throwable): Boolean = excludedExceptions match { 53 | case s if s.nonEmpty => 54 | !excludedExceptions.exists(_.isAssignableFrom(e.getClass)) 55 | case _ => 56 | true 57 | } 58 | impl(limit = limit, sleepInMillis = sleepInMillis)(allowRetry)(f) 59 | } 60 | 61 | /** 62 | * Retry on all IOExceptions that are NOT listed in 63 | * the excludedExceptions list. 64 | * Non-IOException will immediately throw. 65 | */ 66 | private[sbt] def io[@specialized A1](f: => A1, excludedExceptions: Class[? <: IOException]*): A1 = 67 | io(f, limit, excludedExceptions*) 68 | 69 | /** 70 | * Retry on all IOExceptions that are NOT listed in 71 | * the excludedExceptions list. 72 | * Non-IOException will immediately throw. 73 | */ 74 | private[sbt] def io[@specialized A1]( 75 | f: => A1, 76 | limit: Int, 77 | excludedExceptions: Class[? <: IOException]*, 78 | ): A1 = io(f, limit, defaultSleepInMillis, excludedExceptions*) 79 | 80 | /** 81 | * Retry on all IOExceptions that are NOT listed in 82 | * the excludedExceptions list. 83 | * Non-IOException will immediately throw. 84 | */ 85 | private[sbt] def io[@specialized A1]( 86 | f: => A1, 87 | limit: Int, 88 | sleepInMillis: Long, 89 | excludedExceptions: Class[? <: IOException]*, 90 | ): A1 = { 91 | def allowRetry(e: Throwable): Boolean = 92 | e match { 93 | case ioe: IOException => 94 | excludedExceptions match { 95 | case s if s.nonEmpty => 96 | !excludedExceptions.exists(_.isAssignableFrom(ioe.getClass)) 97 | case _ => 98 | true 99 | } 100 | case _ => false 101 | } 102 | impl(limit = limit, sleepInMillis = sleepInMillis)(allowRetry)(f) 103 | } 104 | 105 | private def impl[@specialized A1]( 106 | limit: Int, 107 | sleepInMillis: Long, 108 | )(allowRetry: Throwable => Boolean)(f: => A1): A1 = { 109 | require(limit >= 1, "limit must be 1 or higher: was: " + limit) 110 | var attempt = 1 111 | var firstException: Throwable = null 112 | while (attempt <= limit) { 113 | try { 114 | return f 115 | } catch { 116 | case NonFatal(e) if allowRetry(e) => 117 | if (firstException == null) firstException = e 118 | // https://github.com/sbt/io/issues/295 119 | // On Windows, we're seeing java.nio.file.AccessDeniedException with sleep(0). 120 | Thread.sleep(sleepInMillis) 121 | attempt += 1 122 | } 123 | } 124 | throw firstException 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /io/src/main/scala/sbt/internal/nio/FileCache.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * sbt IO 3 | * Copyright Scala Center, Lightbend, and Mark Harrah 4 | * 5 | * Licensed under Apache License 2.0 6 | * SPDX-License-Identifier: Apache-2.0 7 | * 8 | * See the NOTICE file distributed with this work for 9 | * additional information regarding copyright ownership. 10 | */ 11 | 12 | package sbt.internal.nio 13 | 14 | import java.nio.file.{ Path, Paths } 15 | import java.util 16 | import java.util.Collections 17 | import java.util.concurrent.{ ConcurrentHashMap, ConcurrentSkipListMap } 18 | 19 | import sbt.internal.nio.FileEvent.{ Creation, Deletion, Update } 20 | import sbt.internal.nio.FileCache.{ GlobOps => FileCacheGlobOps } 21 | import sbt.nio.file.FileAttributes.NonExistent 22 | import sbt.nio.file.{ AnyPath, FileAttributes, FileTreeView, Glob, RecursiveGlob } 23 | import sbt.nio.file.Glob.{ GlobOps => GlobGlobOps } 24 | 25 | import scala.collection.JavaConverters._ 26 | import scala.collection.mutable 27 | 28 | private[nio] class FileCache[+T](converter: Path => T, globs: mutable.Set[Glob]) { 29 | def this(converter: Path => T) = 30 | this(converter, ConcurrentHashMap.newKeySet[Glob].asScala) 31 | import FileCache._ 32 | private[this] val files = 33 | Collections.synchronizedSortedMap(new ConcurrentSkipListMap[Path, T]) 34 | private[this] val view: FileTreeView.Nio[FileAttributes] = FileTreeView.default 35 | private[nio] def update( 36 | path: Path, 37 | attributes: FileAttributes, 38 | ): Seq[FileEvent[T]] = { 39 | if (globInclude(path)) { 40 | files.synchronized { 41 | val subMap = files.subMap(path, ceiling(path)) 42 | val exists = attributes != NonExistent 43 | subMap.get(path) match { 44 | case null if exists => 45 | add(updateGlob(path), attributes) 46 | subMap.asScala.map { case (p, a) => Creation(p, a) }.toIndexedSeq 47 | case null => Nil // we weren't monitoring this no longer extant path 48 | case prev if exists => 49 | Update(path, prev, converter(path)) :: Nil 50 | case _ => 51 | remove(subMap).map { case (p, a) => Deletion(p, a) } 52 | } 53 | } 54 | } else { 55 | Nil 56 | } 57 | } 58 | private[nio] def refresh(glob: Glob): Seq[FileEvent[T]] = synchronized { 59 | val path = glob.base 60 | if (globInclude(path)) { 61 | val subMap = files.subMap(path, ceiling(path)) 62 | val previous = subMap.asScala.toMap 63 | subMap.clear() 64 | FileAttributes(path).foreach(add(glob, _)) 65 | val current = subMap.asScala.toMap 66 | val result = new util.ArrayList[FileEvent[T]].asScala 67 | previous.foreach { case (p, prev) => 68 | current.get(p) match { 69 | case Some(newPair) if prev != newPair => 70 | result += Update(p, prev, newPair) 71 | case None => result += Deletion(p, prev) 72 | case _ => 73 | } 74 | } 75 | current.foreach { case (p, newAttributes) => 76 | previous.get(p) match { 77 | case None => result += Creation(p, newAttributes) 78 | case _ => 79 | } 80 | } 81 | result.toVector 82 | } else { 83 | Nil 84 | } 85 | } 86 | private[nio] def list(glob: Glob): Seq[(Path, T)] = { 87 | files 88 | .subMap(glob.base, ceiling(glob.base)) 89 | .asScala 90 | .toIndexedSeq 91 | } 92 | private[nio] def register(glob: Glob): Unit = { 93 | val unfiltered = glob.range._2 match { 94 | case Int.MaxValue => Glob(glob.base, RecursiveGlob) 95 | case d => (1 to d).foldLeft(Glob(glob.base)) { case (g, _) => g / AnyPath } 96 | } 97 | if (!globs.exists(_ covers unfiltered) && globs.add(unfiltered)) { 98 | FileAttributes(glob.base).foreach(add(unfiltered, _)) 99 | } 100 | } 101 | private[nio] def unregister(glob: Glob): Unit = { 102 | if (globs.remove(glob)) { 103 | files.synchronized { 104 | val subMap = files.subMap(glob.base, ceiling(glob.base)) 105 | val filter = globExcludes 106 | val toRemove = subMap.asScala.collect { case (k, _) if filter(k) => k } 107 | toRemove.foreach(subMap.remove) 108 | } 109 | } 110 | } 111 | private[this] def remove(subMap: util.SortedMap[Path, T]): Seq[(Path, T)] = { 112 | val allEntries = subMap.asScala.toIndexedSeq 113 | allEntries.foreach { case (p, _) => subMap.remove(p) } 114 | allEntries 115 | } 116 | private[this] def add(glob: Glob, fileAttributes: FileAttributes): Unit = { 117 | if (fileAttributes != NonExistent) { 118 | val newFiles = new util.HashMap[Path, T] 119 | val asScala = newFiles.asScala 120 | asScala += (glob.base -> converter(glob.base)) 121 | if (fileAttributes.isDirectory) 122 | newFiles.asScala ++= view.list(glob).map { case (p, _) => p -> converter(p) } 123 | files.putAll(newFiles) 124 | } 125 | } 126 | private[this] def globInclude: Path => Boolean = { path => 127 | globs.exists(g => g.matches(path) || g.base == path) 128 | } 129 | private[this] def globExcludes: Path => Boolean = { path => 130 | !globs.exists(g => g.matches(path) || g.base == path) 131 | } 132 | private[this] def updateGlob(path: Path): Glob = { 133 | val depth = globs.toIndexedSeq.view 134 | .map(g => 135 | if (path.startsWith(g.base)) { 136 | if (path == g.base) g.range._2 137 | else 138 | g.range._2 match { 139 | case Int.MaxValue => Int.MaxValue 140 | case d => d - g.base.relativize(path).getNameCount 141 | } 142 | } else Int.MinValue 143 | ) 144 | .min 145 | depth match { 146 | case Int.MaxValue => Glob(path, RecursiveGlob) 147 | case d => (1 to d).foldLeft(Glob(path)) { case (g, _) => g / AnyPath } 148 | } 149 | } 150 | private[this] val ceilingChar = (java.io.File.separatorChar.toInt + 1).toChar 151 | // This is a mildly hacky way of specifying an upper bound for children of a path 152 | private[this] def ceiling(path: Path): Path = Paths.get(path.toString + ceilingChar) 153 | } 154 | private[nio] object FileCache { 155 | private implicit class GlobOps(val glob: Glob) extends AnyVal { 156 | def covers(other: Glob): Boolean = { 157 | val (leftBase, rightBase) = (glob.base, other.base) 158 | val (leftMaxDepth, rightMaxDepth) = (glob.range._2, other.range._2) 159 | rightBase.startsWith(leftBase) && { 160 | (leftBase == rightBase && leftMaxDepth >= rightMaxDepth) || { 161 | val depth = leftBase.relativize(rightBase).getNameCount 162 | leftMaxDepth >= rightMaxDepth - depth 163 | } 164 | } 165 | } 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /io/src/main/scala/sbt/internal/nio/FileEvent.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * sbt IO 3 | * Copyright Scala Center, Lightbend, and Mark Harrah 4 | * 5 | * Licensed under Apache License 2.0 6 | * SPDX-License-Identifier: Apache-2.0 7 | * 8 | * See the NOTICE file distributed with this work for 9 | * additional information regarding copyright ownership. 10 | */ 11 | 12 | package sbt.internal.nio 13 | 14 | import java.nio.file.Path 15 | 16 | private[sbt] sealed trait FileEvent[+T] { 17 | import FileEvent._ 18 | def path: Path 19 | def attributes: T 20 | def exists: Boolean 21 | def occurredAt: Deadline 22 | private[sbt] def map[U](f: (Path, T) => U): FileEvent[U] = this match { 23 | case c: Creation[T] => Creation(path, f(path, c.attributes), c.occurredAt) 24 | case d: Deletion[T] => Deletion(path, f(path, d.attributes), d.occurredAt) 25 | case u: Update[T] => 26 | Update(path, f(path, u.previousAttributes), f(path, u.attributes), u.occurredAt) 27 | } 28 | } 29 | private[sbt] object FileEvent { 30 | def unapply[T](event: FileEvent[T]): Option[(Path, T)] = 31 | event match { 32 | case Creation(path, attributes) => Some((path, attributes)) 33 | case Deletion(path, attributes) => Some((path, attributes)) 34 | case Update(path, _, attributes) => Some((path, attributes)) 35 | } 36 | private[sbt] abstract case class Creation[+T] private[FileEvent] ( 37 | override val path: Path, 38 | attributes: T 39 | ) extends FileEvent[T] { 40 | override def exists: Boolean = true 41 | } 42 | private[sbt] object Creation { 43 | def apply[T](path: Path, attributes: T)(implicit timeSource: TimeSource): Creation[T] = 44 | new Creation(path, attributes) { override val occurredAt: Deadline = timeSource.now } 45 | def apply[T](path: Path, attributes: T, deadline: Deadline): Creation[T] = 46 | new Creation(path, attributes) { override val occurredAt: Deadline = deadline } 47 | } 48 | private[sbt] abstract case class Update[+T] private[FileEvent] ( 49 | override val path: Path, 50 | previousAttributes: T, 51 | attributes: T 52 | ) extends FileEvent[T] { 53 | override def exists: Boolean = true 54 | } 55 | private[sbt] object Update { 56 | def apply[T](path: Path, previousAttributes: T, attributes: T)(implicit 57 | timeSource: TimeSource 58 | ): Update[T] = 59 | new Update(path, previousAttributes, attributes) { 60 | override val occurredAt: Deadline = timeSource.now 61 | } 62 | def apply[T](path: Path, previousAttributes: T, attributes: T, deadline: Deadline): Update[T] = 63 | new Update(path, previousAttributes, attributes) { 64 | override val occurredAt: Deadline = deadline 65 | } 66 | } 67 | private[sbt] abstract case class Deletion[+T] private[FileEvent] ( 68 | override val path: Path, 69 | override val attributes: T 70 | ) extends FileEvent[T] { 71 | override def exists: Boolean = false 72 | } 73 | private[sbt] object Deletion { 74 | def apply[T](path: Path, attributes: T)(implicit timeSource: TimeSource): Deletion[T] = 75 | new Deletion(path, attributes) { override val occurredAt: Deadline = timeSource.now } 76 | def apply[T](path: Path, attributes: T, deadline: Deadline): Deletion[T] = 77 | new Deletion(path, attributes) { override val occurredAt: Deadline = deadline } 78 | } 79 | 80 | } 81 | -------------------------------------------------------------------------------- /io/src/main/scala/sbt/internal/nio/FileTreeRepository.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * sbt IO 3 | * Copyright Scala Center, Lightbend, and Mark Harrah 4 | * 5 | * Licensed under Apache License 2.0 6 | * SPDX-License-Identifier: Apache-2.0 7 | * 8 | * See the NOTICE file distributed with this work for 9 | * additional information regarding copyright ownership. 10 | */ 11 | 12 | package sbt.internal.nio 13 | 14 | import sbt.io.WatchService 15 | import sbt.nio.file.{ FileAttributes, FileTreeView } 16 | 17 | /** 18 | * Monitors registered directories for file changes. A typical implementation will keep an 19 | * in memory cache of the file system that can be queried in [[FileTreeRepository!.list]]. The 20 | * [[FileTreeRepository#register]] method adds monitoring for a particular cache. A filter may be 21 | * provided so that the cache doesn't waste memory on files the user doesn't care about. The 22 | * cache may be shared across a code base so there additional apis for adding filters or changing 23 | * the recursive property of a directory. 24 | * 25 | * @tparam T the type of the 26 | */ 27 | private[sbt] trait FileTreeRepository[+T] 28 | extends FileTreeView.Nio[T] 29 | with Registerable[FileEvent[T]] 30 | with Observable[FileEvent[T]] 31 | with AutoCloseable 32 | 33 | private[sbt] object FileTreeRepository { 34 | 35 | /** 36 | * Create a [[FileTreeRepository]]. The generated repository will cache the file system tree for the 37 | * monitored directories. 38 | * 39 | * @return a file repository. 40 | */ 41 | private[sbt] def default: FileTreeRepository[FileAttributes] = new FileTreeRepositoryImpl 42 | 43 | /** 44 | * Create a [[FileTreeRepository]]. The generated repository will cache the file system tree for the 45 | * monitored directories. 46 | * 47 | * @return a file repository. 48 | */ 49 | private[sbt] def legacy: FileTreeRepository[FileAttributes] = 50 | new LegacyFileTreeRepository((_: Any) => (), WatchService.default) 51 | 52 | /** 53 | * Create a [[FileTreeRepository]] with a provided logger. The generated repository will cache 54 | * the file system tree for the monitored directories. 55 | * 56 | * @param logger used to log file events 57 | * @param watchService the [[WatchService]] to monitor for file system events 58 | * @tparam T the generic type of the custom file attributes 59 | * @return a file repository. 60 | */ 61 | private[sbt] def legacy[T]( 62 | logger: WatchLogger, 63 | watchService: WatchService 64 | ): FileTreeRepository[FileAttributes] = 65 | new LegacyFileTreeRepository(logger, watchService) 66 | 67 | } 68 | -------------------------------------------------------------------------------- /io/src/main/scala/sbt/internal/nio/FileTreeRepositoryImpl.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * sbt IO 3 | * Copyright Scala Center, Lightbend, and Mark Harrah 4 | * 5 | * Licensed under Apache License 2.0 6 | * SPDX-License-Identifier: Apache-2.0 7 | * 8 | * See the NOTICE file distributed with this work for 9 | * additional information regarding copyright ownership. 10 | */ 11 | 12 | package sbt.internal.nio 13 | 14 | import java.io.IOException 15 | import java.nio.file.{ Path => NioPath } 16 | import java.util.concurrent.ConcurrentHashMap 17 | import java.util.concurrent.atomic.AtomicBoolean 18 | 19 | import com.swoval.files.FileTreeDataViews.CacheObserver 20 | import com.swoval.files.{ FileTreeDataViews, FileTreeRepositories, TypedPath => STypedPath } 21 | import com.swoval.functional.Filters 22 | import sbt.internal.nio.FileEvent.{ Creation, Deletion, Update } 23 | import sbt.internal.nio.SwovalConverters._ 24 | import sbt.nio.file.{ FileAttributes, Glob } 25 | import sbt.nio.file.Glob.GlobOps 26 | 27 | import scala.collection.JavaConverters._ 28 | import scala.collection.immutable.VectorBuilder 29 | import scala.util.Properties 30 | 31 | /** 32 | * The default implementation of [[FileTreeRepository]]. It delegates all of its methods to the 33 | * [[https://swoval.github.io/files/jvm/com/swoval/files/FileTreeRepository.html swoval FileTreeRepository]]. 34 | * 35 | * @tparam T the type of the values. 36 | */ 37 | private[sbt] class FileTreeRepositoryImpl[T] extends FileTreeRepository[FileAttributes] { 38 | private[this] val closed = new AtomicBoolean(false) 39 | private[this] val underlying = FileTreeRepositories.get[FileAttributes]( 40 | (typedPath: STypedPath) => { 41 | FileAttributes( 42 | isDirectory = typedPath.isDirectory, 43 | isOther = false, 44 | isRegularFile = typedPath.isFile, 45 | isSymbolicLink = typedPath.isSymbolicLink 46 | ) 47 | }, 48 | true 49 | ) 50 | private[this] val observers = new Observers[FileEvent[FileAttributes]] 51 | private[this] val registered = ConcurrentHashMap.newKeySet[NioPath].asScala 52 | private[this] val isMac = Properties.isMac 53 | 54 | underlying.addCacheObserver(new CacheObserver[FileAttributes] { 55 | override def onCreate(newEntry: FileTreeDataViews.Entry[FileAttributes]): Unit = { 56 | val path = newEntry.getTypedPath.getPath 57 | newEntry.getValue.asScala.foreach { v => 58 | observers.onNext(Creation(path, v)) 59 | } 60 | () 61 | } 62 | override def onDelete(oldEntry: FileTreeDataViews.Entry[FileAttributes]): Unit = { 63 | val path = oldEntry.getTypedPath.getPath 64 | oldEntry.getValue.asScala.foreach { v => 65 | observers.onNext(Deletion(path, v)) 66 | } 67 | () 68 | } 69 | override def onUpdate( 70 | oldEntry: FileTreeDataViews.Entry[FileAttributes], 71 | newEntry: FileTreeDataViews.Entry[FileAttributes] 72 | ): Unit = { 73 | val path = newEntry.getTypedPath.getPath 74 | val oldEither = oldEntry.getValue.asScala 75 | val newEither = newEntry.getValue.asScala 76 | oldEither match { 77 | case Right(o) => 78 | newEither match { 79 | case Right(n) => observers.onNext(Update(path, o, n)) 80 | case _ => observers.onNext(Deletion(path, o)) 81 | } 82 | case _ => 83 | newEither match { 84 | case Right(n) => observers.onNext(Creation(path, n)) 85 | case _ => 86 | } 87 | } 88 | } 89 | override def onError(exception: IOException): Unit = {} 90 | }: CacheObserver[FileAttributes]) 91 | override def addObserver(observer: Observer[FileEvent[FileAttributes]]): AutoCloseable = { 92 | throwIfClosed("addObserver") 93 | observers.addObserver(observer) 94 | } 95 | override def list(path: NioPath): Seq[(NioPath, FileAttributes)] = { 96 | throwIfClosed("list") 97 | val res = new VectorBuilder[(NioPath, FileAttributes)] 98 | underlying 99 | .listEntries(path, 0, Filters.AllPass) 100 | .iterator 101 | .asScala 102 | .foreach { e => 103 | val tp = e.getTypedPath 104 | val path = tp.getPath 105 | e.getValue.asScala match { 106 | case Right(t: FileAttributes @unchecked) => res += path -> t 107 | case _ => 108 | } 109 | } 110 | res.result() 111 | } 112 | override def register(glob: Glob): Either[IOException, Observable[FileEvent[FileAttributes]]] = { 113 | throwIfClosed("register") 114 | val base = glob.base 115 | if (isMac) { 116 | // workaround for https://github.com/sbt/sbt/issues/4603 117 | val parent = glob.base.getParent 118 | if (!registered.contains(parent)) { 119 | if ( 120 | registered.exists(path => 121 | path.getParent == parent && { 122 | val leftFileName = path.getFileName.toString 123 | val rightFileName = base.getFileName.toString 124 | leftFileName != rightFileName && (leftFileName 125 | .startsWith(rightFileName) || rightFileName.startsWith(leftFileName)) 126 | } 127 | ) 128 | ) { 129 | register(Glob(parent)) 130 | } 131 | } 132 | } 133 | underlying.register(base, glob.range.toSwovalDepth).asScala match { 134 | case Right(_) => 135 | registered.add(base) 136 | new RegisterableObservable(observers).register(glob) 137 | case Left(ex) => Left(ex) 138 | } 139 | } 140 | override def close(): Unit = if (closed.compareAndSet(false, true)) { 141 | underlying.close() 142 | } 143 | private[this] def throwIfClosed(method: String): Unit = 144 | if (closed.get()) { 145 | val ex = new IllegalStateException(s"Tried to invoke $method on closed repository $this") 146 | ex.printStackTrace() 147 | throw ex 148 | } 149 | 150 | } 151 | -------------------------------------------------------------------------------- /io/src/main/scala/sbt/internal/nio/Globs.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * sbt IO 3 | * Copyright Scala Center, Lightbend, and Mark Harrah 4 | * 5 | * Licensed under Apache License 2.0 6 | * SPDX-License-Identifier: Apache-2.0 7 | * 8 | * See the NOTICE file distributed with this work for 9 | * additional information regarding copyright ownership. 10 | */ 11 | 12 | package sbt.internal.nio 13 | 14 | import java.nio.file.Path 15 | 16 | import sbt.io._ 17 | import sbt.nio.file.Glob.FullFileGlob 18 | import sbt.nio.file.RelativeGlob.{ Matcher, NoPath } 19 | import sbt.nio.file.{ AnyPath, Glob, RecursiveGlob, RelativeGlob } 20 | 21 | private[sbt] object Globs { 22 | private[sbt] def apply(base: Path, recursive: Boolean, fileFilter: FileFilter): Glob = { 23 | fileFilterToRelativeGlob(fileFilter) match { 24 | case Some(relativeGlob) => 25 | Glob(base, if (recursive) RecursiveGlob / relativeGlob else relativeGlob) 26 | case None => 27 | FullFileGlob(base, recursive, fileFilter) 28 | } 29 | } 30 | private[sbt] def fileFilterToRelativeGlob(fileFilter: FileFilter): Option[RelativeGlob] = 31 | fileFilter match { 32 | case nameFilter: NameFilter => nameFilterToRelativeGlob(nameFilter) 33 | case af: AndFilter => 34 | if (af.left == NothingFilter || af.right == NothingFilter) Some(NoPath) 35 | else if (af.left == AllPassFilter) fileFilterToRelativeGlob(af.right) 36 | else if (af.right == AllPassFilter) fileFilterToRelativeGlob(af.left) 37 | else { 38 | (fileFilterToRelativeGlob(af.left), fileFilterToRelativeGlob(af.right)) match { 39 | case (no @ Some(NoPath), _) => no 40 | case (_, no @ Some(NoPath)) => no 41 | case (Some(AnyPath), right) => right 42 | case (left, Some(AnyPath)) => left 43 | case (Some(left: Matcher), Some(right: Matcher)) => Some(Matcher.and(left, right)) 44 | case _ => None 45 | } 46 | } 47 | case nf: NotFilter => 48 | nf.fileFilter match { 49 | case NothingFilter => Some(AnyPath) 50 | case AllPassFilter => Some(NoPath) 51 | case f => 52 | fileFilterToRelativeGlob(f).collect { case m: Matcher => 53 | Matcher.not(m) 54 | } 55 | } 56 | case of: OrFilter => 57 | if (of.left == AllPassFilter || of.right == AllPassFilter) Some(AnyPath) 58 | else if (of.left == NothingFilter) fileFilterToRelativeGlob(of.right) 59 | else if (of.right == NothingFilter) fileFilterToRelativeGlob(of.left) 60 | else { 61 | (fileFilterToRelativeGlob(of.left), fileFilterToRelativeGlob(of.right)) match { 62 | case (Some(NoPath), right) => right 63 | case (left, Some(NoPath)) => left 64 | case (any @ Some(AnyPath), _) => any 65 | case (_, any @ Some(AnyPath)) => any 66 | case (Some(left: Matcher), Some(right: Matcher)) => Some(Matcher.or(left, right)) 67 | case _ => None 68 | } 69 | } 70 | case _ => None 71 | } 72 | private[sbt] def nameFilterToRelativeGlob(nameFilter: NameFilter): Option[Matcher] = 73 | nameFilter match { 74 | case AllPassFilter => Some(AnyPath) 75 | case af: AndNameFilter => 76 | if (af.left == NothingFilter || af.right == NothingFilter) Some(NoPath) 77 | else if (af.left == AllPassFilter) nameFilterToRelativeGlob(af.right) 78 | else if (af.right == AllPassFilter) nameFilterToRelativeGlob(af.left) 79 | else 80 | (nameFilterToRelativeGlob(af.left), nameFilterToRelativeGlob(af.right)) match { 81 | case (Some(AnyPath), right) => right 82 | case (left, Some(AnyPath)) => left 83 | case (no @ Some(NoPath), _) => no 84 | case (_, no @ Some(NoPath)) => no 85 | case (Some(l), Some(r)) => Some(Matcher.and(l, r)) 86 | case _ => None 87 | } 88 | case ef: ExactFilter => Some(Matcher(ef.matchName)) 89 | case ef: ExtensionFilter => 90 | ef.extensions match { 91 | case extensions if extensions.length == 1 => Some(Matcher(s"*.${extensions.head}")) 92 | case extensions => Some(Matcher(s"*.${extensions.mkString("{", ",", "}")}")) 93 | } 94 | case NothingFilter => Some(NoPath) 95 | case nf: NotNameFilter => 96 | if (nf.fileFilter == NothingFilter) Some(AnyPath) 97 | else nameFilterToRelativeGlob(nf.fileFilter).map(Matcher.not) 98 | case of: OrNameFilter => 99 | if (of.left == AllPassFilter || of.right == AllPassFilter) Some(AnyPath) 100 | else if (of.left == NothingFilter) nameFilterToRelativeGlob(of.right) 101 | else if (of.right == NothingFilter) nameFilterToRelativeGlob(of.left) 102 | else 103 | (nameFilterToRelativeGlob(of.left), nameFilterToRelativeGlob(of.right)) match { 104 | case (any @ Some(AnyPath), _) => any 105 | case (_, any @ Some(AnyPath)) => any 106 | case (Some(NoPath), right) => right 107 | case (left, Some(NoPath)) => left 108 | case (Some(l), Some(r)) => Some(Matcher.or(l, r)) 109 | case _ => None 110 | } 111 | case pf: PatternFilter => Some(Matcher(s"${pf.parts.mkString("*")}")) 112 | case pf: PrefixFilter => Some(Matcher(s"${pf.prefix}*")) 113 | case sf: SimpleFilter => Some(Matcher(sf.acceptFunction)) 114 | case sf: SuffixFilter => Some(Matcher(s"*${sf.suffix}")) 115 | case _ => None 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /io/src/main/scala/sbt/internal/nio/LegacyFileTreeRepository.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * sbt IO 3 | * Copyright Scala Center, Lightbend, and Mark Harrah 4 | * 5 | * Licensed under Apache License 2.0 6 | * SPDX-License-Identifier: Apache-2.0 7 | * 8 | * See the NOTICE file distributed with this work for 9 | * additional information regarding copyright ownership. 10 | */ 11 | 12 | package sbt.internal.nio 13 | 14 | import java.io.IOException 15 | import java.nio.file.{ Path, WatchKey } 16 | import java.util.concurrent.ConcurrentHashMap 17 | 18 | import sbt.internal.io._ 19 | import sbt.internal.nio.FileEvent.Deletion 20 | import sbt.io._ 21 | import sbt.nio.file.FileAttributes.NonExistent 22 | import sbt.nio.file.{ FileAttributes, FileTreeView, Glob } 23 | 24 | import scala.collection.JavaConverters._ 25 | import scala.concurrent.duration._ 26 | 27 | /** 28 | * A [[FileTreeRepository]] that can be used to emulate the behavior of sbt < 1.3.0 The list methods 29 | * will always poll the file system and the monitoring will be handled by a 30 | * [[WatchServiceBackedObservable]]. 31 | */ 32 | private[sbt] class LegacyFileTreeRepository(logger: WatchLogger, watchService: WatchService) 33 | extends FileTreeRepository[FileAttributes] { 34 | private[this] val view: FileTreeView.Nio[FileAttributes] = FileTreeView.default 35 | private[this] val globs = ConcurrentHashMap.newKeySet[Glob].asScala 36 | private[this] val fileCache = new FileCache(p => FileAttributes(p).getOrElse(NonExistent), globs) 37 | private[this] val observable 38 | : Observable[FileEvent[FileAttributes]] & Registerable[FileEvent[FileAttributes]] = 39 | new WatchServiceBackedObservable( 40 | new NewWatchState(globs, watchService, new ConcurrentHashMap[Path, WatchKey].asScala), 41 | 100.millis, 42 | closeService = true, 43 | logger 44 | ) 45 | private[this] val observers = new Observers[FileEvent[FileAttributes]] 46 | private[this] val handle = 47 | observable.addObserver((event: FileEvent[FileAttributes]) => { 48 | val attributes = event match { 49 | case _: Deletion[?] => NonExistent 50 | case _ => event.attributes 51 | } 52 | val events: Seq[FileEvent[FileAttributes]] = fileCache.update(event.path, attributes) 53 | events.foreach(observers.onNext) 54 | }) 55 | override def close(): Unit = { 56 | handle.close() 57 | observable.close() 58 | } 59 | override def register(glob: Glob): Either[IOException, Observable[FileEvent[FileAttributes]]] = { 60 | fileCache.register(glob) 61 | observable.register(glob).foreach(_.close()) 62 | new RegisterableObservable(observers).register(glob) 63 | } 64 | override def list(path: Path): Seq[(Path, FileAttributes)] = view.list(path) 65 | 66 | /** 67 | * Add callbacks to be invoked on file events. 68 | * 69 | * @param observer the callbacks to invoke 70 | * @return a handle to the callback. 71 | */ 72 | override def addObserver(observer: Observer[FileEvent[FileAttributes]]): AutoCloseable = 73 | observers.addObserver(observer) 74 | } 75 | -------------------------------------------------------------------------------- /io/src/main/scala/sbt/internal/nio/Observers.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * sbt IO 3 | * Copyright Scala Center, Lightbend, and Mark Harrah 4 | * 5 | * Licensed under Apache License 2.0 6 | * SPDX-License-Identifier: Apache-2.0 7 | * 8 | * See the NOTICE file distributed with this work for 9 | * additional information regarding copyright ownership. 10 | */ 11 | 12 | package sbt.internal.nio 13 | 14 | import java.io.IOException 15 | import java.nio.file.{ Path => NioPath } 16 | import java.util.WeakHashMap 17 | import java.util.concurrent.ConcurrentHashMap 18 | import java.util.concurrent.atomic.AtomicInteger 19 | 20 | import sbt.nio.file.Glob 21 | import sbt.nio.file.Glob.GlobOps 22 | 23 | import scala.util.control.NonFatal 24 | 25 | @FunctionalInterface 26 | private[sbt] trait Observer[-T] extends AutoCloseable { 27 | 28 | /** 29 | * Run a callback 30 | * @param t the generic type of events 31 | */ 32 | def onNext(t: T): Unit 33 | 34 | /** 35 | * This is for managed observers that may need to be removed once the caller is done with them. 36 | * See [[Observers]]. 37 | */ 38 | override def close(): Unit = {} 39 | } 40 | 41 | /** 42 | * An object that monitors a file system. The interface is very similar to that provided by other 43 | * libraries/frameworks, such as [[http://reactivex.io/intro.html rxJava]]. When it detects changes 44 | * in the file system, it will invoke a set of user specified callbacks. The Observable also 45 | * allows the user to add and removes paths to monitor. 46 | * 47 | * @tparam T the generic type of observable values 48 | */ 49 | private[sbt] trait Observable[+T] extends AutoCloseable { 50 | 51 | /** 52 | * Add callbacks to be invoked on file events. 53 | * 54 | * @param observer the callbacks to invoke 55 | * @return a handle to the callback. 56 | */ 57 | def addObserver(observer: Observer[T]): AutoCloseable 58 | } 59 | 60 | private[sbt] trait ObservablePaths[+T] extends Observable[(NioPath, T)] 61 | 62 | private[sbt] object Observable { 63 | def map[T, R](observable: Observable[T], f: T => R): Observable[R] = new Observable[R] { 64 | override def addObserver(observer: Observer[R]): AutoCloseable = 65 | observable.addObserver(t => observer.onNext(f(t))) 66 | override def close(): Unit = observable.close() 67 | } 68 | def filter[T](observable: Observable[T], f: T => Boolean): Observable[T] = new Observable[T] { 69 | override def addObserver(observer: Observer[T]): AutoCloseable = 70 | observable.addObserver(t => if (f(t)) observer.onNext(t)) 71 | override def close(): Unit = observable.close() 72 | } 73 | } 74 | 75 | /** 76 | * Aggregates a collection of [[Observer]]s into a single [[Observer]]. The callbacks for the 77 | * generated [[Observer]] invoke the corresponding callback for each of the [[Observer]]s that 78 | * are added via [[addObserver]]. 79 | * 80 | * @tparam T the generic type of value instances for the [[FileTreeRepository]] 81 | */ 82 | private[sbt] class Observers[T] extends Observer[T] with Observable[T] { 83 | private class Handle(id: Int) extends AutoCloseable { 84 | override def close(): Unit = { 85 | observers.remove(id) 86 | () 87 | } 88 | } 89 | private[this] val id = new AtomicInteger(0) 90 | private[this] val observers = new ConcurrentHashMap[Int, Observer[T]] 91 | private[this] val observables = new WeakHashMap[AutoCloseable, Unit] 92 | 93 | private[sbt] def addObservable(observable: Observable[T]): AutoCloseable = 94 | observables.synchronized { 95 | val handle = observable.addObserver(this) 96 | observables.put(handle, ()) 97 | handle 98 | } 99 | override def addObserver(observer: Observer[T]): AutoCloseable = { 100 | val observerId = id.incrementAndGet() 101 | observers.put(observerId, observer) 102 | new Handle(observerId) 103 | } 104 | 105 | override def close(): Unit = { 106 | observers.clear() 107 | observables.synchronized { 108 | observables.keySet.forEach(_.close()) 109 | observables.clear() 110 | } 111 | } 112 | override def toString: String = 113 | s"Observers(observers = ${observers.values}, observables = ${observables.keySet})" 114 | override def onNext(t: T): Unit = observers.values.forEach { o => 115 | try o.onNext(t) 116 | catch { case NonFatal(_) => } 117 | } 118 | } 119 | private[sbt] class RegisterableObservable[T](val delegate: Observers[FileEvent[T]]) extends AnyVal { 120 | def register(glob: Glob): Either[IOException, Observable[FileEvent[T]]] = 121 | Registerable(glob, delegate) 122 | } 123 | 124 | /** 125 | * A dynamically configured monitor of the file system. New paths can be added and removed from 126 | * monitoring with register / unregister. 127 | */ 128 | private[sbt] trait Registerable[+T] extends AutoCloseable { 129 | 130 | /** 131 | * Register a glob for monitoring. 132 | * 133 | * @param glob Glob 134 | * @return an Either that is a Right when register has no errors and a Left if an IOException is 135 | * thrown while registering the path. The result should be true if the path has 136 | * never been previously registered or if the recursive flag flips from false to true. 137 | */ 138 | def register(glob: Glob): Either[IOException, Observable[T]] 139 | } 140 | private[sbt] object Registerable { 141 | def apply[T]( 142 | glob: Glob, 143 | delegate: Observers[FileEvent[T]] 144 | ): Either[IOException, Observable[FileEvent[T]]] = { 145 | val filter = glob.toFileFilter 146 | val underlying = new Observers[FileEvent[T]] 147 | val observers = 148 | Observable.filter(underlying, (e: FileEvent[T]) => filter.accept(e.path.toFile)) 149 | val handle = delegate.addObserver(underlying) 150 | 151 | Right(new Observable[FileEvent[T]] { 152 | override def addObserver(observer: Observer[FileEvent[T]]): AutoCloseable = 153 | observers.addObserver(observer) 154 | override def close(): Unit = handle.close() 155 | }) 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /io/src/main/scala/sbt/internal/nio/PollingWatchService.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * sbt IO 3 | * Copyright Scala Center, Lightbend, and Mark Harrah 4 | * 5 | * Licensed under Apache License 2.0 6 | * SPDX-License-Identifier: Apache-2.0 7 | * 8 | * See the NOTICE file distributed with this work for 9 | * additional information regarding copyright ownership. 10 | */ 11 | 12 | package sbt.internal.nio 13 | 14 | import java.nio.file.StandardWatchEventKinds._ 15 | import java.nio.file.{ WatchService => _, _ } 16 | import java.util 17 | import java.util.concurrent._ 18 | import java.util.concurrent.atomic.{ AtomicBoolean, AtomicReference } 19 | import java.util.{ List => JList } 20 | 21 | import sbt.internal.nio.FileEvent.{ Creation, Deletion, Update } 22 | import sbt.io._ 23 | import sbt.nio.file.{ AnyPath, Glob } 24 | 25 | import scala.annotation.tailrec 26 | import scala.collection.JavaConverters._ 27 | import scala.collection.immutable 28 | import scala.concurrent.duration.{ Deadline => _, _ } 29 | import scala.util.Random 30 | 31 | /** A `WatchService` that polls the filesystem every `delay`. */ 32 | private[sbt] class PollingWatchService(delay: FiniteDuration, timeSource: TimeSource) 33 | extends WatchService 34 | with Unregisterable { 35 | def this(delay: FiniteDuration) = this(delay, TimeSource.default) 36 | private[this] implicit def ts: TimeSource = timeSource 37 | private[this] val closed = new AtomicBoolean(false) 38 | private[this] val registered = new ConcurrentHashMap[Path, PollingWatchKey].asScala 39 | private[this] val lastModifiedConverter: Path => Long = p => IO.getModifiedTimeOrZero(p.toFile) 40 | private[this] val pollQueue: util.Queue[PollingWatchKey] = 41 | new LinkedBlockingDeque[PollingWatchKey] 42 | private[this] val random = new Random() 43 | override def close(): Unit = if (closed.compareAndSet(false, true)) { 44 | registered.clear() 45 | } 46 | 47 | override def init(): Unit = { 48 | ensureNotClosed() 49 | } 50 | 51 | override def poll(timeout: Duration): WatchKey = { 52 | ensureNotClosed() 53 | val numKeys = pollQueue.size 54 | val (adjustedTimeout, deadline) = timeout match { 55 | case t: FiniteDuration => t -> (Deadline.now + t) 56 | case _ => (numKeys * 2.millis) -> Deadline.Inf 57 | } 58 | val millis = adjustedTimeout.toMillis 59 | val (batchSize, batchTimeout) = (1 to Int.MaxValue) 60 | .dropWhile(i => (millis * i) / numKeys == 0) 61 | .headOption 62 | .fold(1 -> 1.millis)(i => i -> (adjustedTimeout / i.toLong)) 63 | pollImpl(batchSize, batchTimeout, deadline).orNull 64 | } 65 | 66 | @tailrec 67 | private def pollImpl( 68 | batchSize: Int, 69 | duration: FiniteDuration, 70 | deadline: Deadline 71 | ): Option[WatchKey] = { 72 | pollQueue.poll() match { 73 | case null => None 74 | case key => 75 | pollQueue.add(key) 76 | key.poll() match { 77 | case r if r.isDefined => r 78 | case _ => 79 | if (batchSize > 1) { 80 | pollImpl(batchSize - 1, duration, deadline) 81 | } else if (!deadline.isOverdue) { 82 | Thread.sleep(duration.toMillis) 83 | pollImpl(batchSize, duration, deadline) 84 | } else { 85 | None 86 | } 87 | } 88 | } 89 | } 90 | 91 | override def pollEvents(): Map[WatchKey, immutable.Seq[WatchEvent[Path]]] = { 92 | ensureNotClosed() 93 | registered.values.map(k => (k: WatchKey) -> k.pollEventsImpl.asScala.toVector).toMap 94 | } 95 | 96 | override def register(path: Path, events: WatchEvent.Kind[Path]*): WatchKey = { 97 | ensureNotClosed() 98 | registered.get(path) match { 99 | case Some(k) => k 100 | case None => 101 | val newKey = new PollingWatchKey(path, events*) 102 | registered.put(path, newKey) 103 | pollQueue.add(newKey) 104 | newKey 105 | } 106 | } 107 | override def unregister(path: Path): Unit = { 108 | ensureNotClosed() 109 | registered.remove(path) 110 | pollQueue.removeIf(_.path == path) 111 | () 112 | } 113 | 114 | private def ensureNotClosed(): Unit = 115 | if (closed.get()) throw new ClosedWatchServiceException 116 | 117 | private object Overflow 118 | extends PollingWatchEvent(null, OVERFLOW.asInstanceOf[WatchEvent.Kind[Path]]) 119 | private class PollingWatchKey( 120 | private[PollingWatchService] val path: Path, 121 | eventKinds: WatchEvent.Kind[Path]* 122 | ) extends WatchKey { 123 | private[this] val events = 124 | new ArrayBlockingQueue[FileEvent[Long]](256) 125 | private[this] val hasOverflow = new AtomicBoolean(false) 126 | private[this] lazy val acceptCreate = eventKinds.contains(ENTRY_CREATE) 127 | private[this] lazy val acceptDelete = eventKinds.contains(ENTRY_DELETE) 128 | private[this] lazy val acceptModify = eventKinds.contains(ENTRY_MODIFY) 129 | private[this] val glob = Glob(path, AnyPath) 130 | private[this] val fileCache = 131 | new FileCache[Long](lastModifiedConverter) 132 | fileCache.register(glob) 133 | private[this] def nextPollTime: Deadline = 134 | Deadline.now + random.nextInt(2 * delay.toMillis.toInt).millis 135 | private[this] val lastPolled = new AtomicReference(nextPollTime) 136 | override def cancel(): Unit = { 137 | reset() 138 | registered.remove(path) 139 | () 140 | } 141 | override def isValid: Boolean = true 142 | override def pollEvents(): JList[WatchEvent[?]] = 143 | pollEventsImpl.asInstanceOf[JList[WatchEvent[?]]] 144 | override def reset(): Boolean = events.synchronized { 145 | events.clear() 146 | true 147 | } 148 | override def watchable(): Watchable = path 149 | 150 | private[PollingWatchService] def poll(): Option[WatchKey] = { 151 | lastPolled.get match { 152 | case d if d < Deadline.now => 153 | val res = fileCache.refresh(glob) 154 | lastPolled.set(Deadline.now) 155 | res.foreach(maybeAddEvent) 156 | if (events.isEmpty) None else Some(this) 157 | case _ => None 158 | } 159 | } 160 | private[PollingWatchService] def pollEventsImpl: JList[WatchEvent[Path]] = { 161 | events.synchronized { 162 | val overflow = hasOverflow.getAndSet(false) 163 | val size = events.size + (if (overflow) 1 else 0) 164 | val rawEvents = new util.ArrayList[FileEvent[Long]](size) 165 | events.drainTo(rawEvents) 166 | val res = new util.ArrayList[WatchEvent[Path]](size) 167 | res.addAll(rawEvents.asScala.map { 168 | case Creation(p, _) => new PollingWatchEvent(p, ENTRY_CREATE) 169 | case Deletion(p, _) => new PollingWatchEvent(p, ENTRY_DELETE) 170 | case Update(p, _, _) => new PollingWatchEvent(p, ENTRY_MODIFY) 171 | }.asJava) 172 | if (overflow) res.add(Overflow) 173 | res 174 | } 175 | } 176 | private[PollingWatchService] def maybeAddEvent( 177 | event: FileEvent[Long] 178 | ): Option[PollingWatchKey] = { 179 | def offer(event: FileEvent[Long]): Option[PollingWatchKey] = { 180 | if (!events.synchronized(events.offer(event))) hasOverflow.set(true) 181 | Some(this) 182 | } 183 | event match { 184 | case _: Creation[?] if acceptCreate => offer(event) 185 | case _: Deletion[?] if acceptDelete => offer(event) 186 | case _: Update[?] if acceptModify => offer(event) 187 | case _ => None 188 | } 189 | } 190 | } 191 | 192 | } 193 | 194 | private class PollingWatchEvent( 195 | override val context: Path, 196 | override val kind: WatchEvent.Kind[Path] 197 | ) extends WatchEvent[Path] { 198 | override val count: Int = 1 199 | override def toString: String = kind match { 200 | case ENTRY_CREATE => s"Creation($context)" 201 | case ENTRY_DELETE => s"Deletion($context)" 202 | case ENTRY_MODIFY => s"Modify($context)" 203 | case _ => "Overflow" 204 | } 205 | } 206 | -------------------------------------------------------------------------------- /io/src/main/scala/sbt/internal/nio/SwovalConverters.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * sbt IO 3 | * Copyright Scala Center, Lightbend, and Mark Harrah 4 | * 5 | * Licensed under Apache License 2.0 6 | * SPDX-License-Identifier: Apache-2.0 7 | * 8 | * See the NOTICE file distributed with this work for 9 | * additional information regarding copyright ownership. 10 | */ 11 | 12 | package sbt.internal.nio 13 | 14 | import java.nio.file.{ NoSuchFileException, NotDirectoryException, Path } 15 | 16 | import com.swoval.files.FileTreeViews 17 | import com.swoval.functional.{ Either => SEither } 18 | import sbt.internal.io.Retry 19 | import sbt.nio.file.{ FileAttributes, FileTreeView } 20 | 21 | import scala.collection.immutable.VectorBuilder 22 | 23 | /** 24 | * Utilities for converting between swoval and sbt data types. 25 | */ 26 | private[nio] object SwovalConverters { 27 | implicit class RangeOps(val range: (Int, Int)) extends AnyVal { 28 | def toSwovalDepth: Int = range._2 match { 29 | case Int.MaxValue => Int.MaxValue 30 | case d => d - 1 31 | } 32 | } 33 | implicit class SwovalEitherOps[L, R](val either: SEither[L, R]) extends AnyVal { 34 | def asScala[R0](implicit f: R => R0): Either[L, R0] = either match { 35 | case l: com.swoval.functional.Either.Left[L, R] => 36 | Left(com.swoval.functional.Either.leftProjection(l).getValue) 37 | case r: com.swoval.functional.Either.Right[L, R] => Right(f(r.get())) 38 | } 39 | } 40 | } 41 | private[sbt] object SwovalFileTreeView extends FileTreeView.Nio[FileAttributes] { 42 | private[this] val view = FileTreeViews.getDefault(true) 43 | override def list(path: Path): Seq[(Path, FileAttributes)] = 44 | Retry( 45 | { 46 | val result = new VectorBuilder[(Path, FileAttributes)] 47 | view.list(path, 0, _ => true).forEach { typedPath => 48 | result += typedPath.getPath -> 49 | FileAttributes( 50 | isDirectory = typedPath.isDirectory, 51 | isOther = false, 52 | isRegularFile = typedPath.isFile, 53 | isSymbolicLink = typedPath.isSymbolicLink 54 | ) 55 | } 56 | result.result() 57 | }, 58 | excludedExceptions* 59 | ) 60 | 61 | private val excludedExceptions = 62 | List(classOf[NotDirectoryException], classOf[NoSuchFileException]) 63 | } 64 | -------------------------------------------------------------------------------- /io/src/main/scala/sbt/internal/nio/TimeSource.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * sbt IO 3 | * Copyright Scala Center, Lightbend, and Mark Harrah 4 | * 5 | * Licensed under Apache License 2.0 6 | * SPDX-License-Identifier: Apache-2.0 7 | * 8 | * See the NOTICE file distributed with this work for 9 | * additional information regarding copyright ownership. 10 | */ 11 | 12 | package sbt.internal.nio 13 | 14 | import scala.concurrent.duration._ 15 | 16 | /** 17 | * A factory that generates instances of [[Deadline]]. This will typically just get the current 18 | * time using System.currentTimeMillis but in testing can be replaced with a mutable time source 19 | * that the the test author can manually increment to verify certain behaviors. Without this 20 | * indirection, it is difficult to make the tests deterministic. 21 | */ 22 | private[nio] trait TimeSource { 23 | def now: Deadline 24 | } 25 | private[nio] object TimeSource { 26 | implicit object default extends TimeSource { 27 | override def now: Deadline = new DefaultImpl() 28 | } 29 | 30 | /** 31 | * Use System.currentTimeMillis because we don't really care that much about precision and 32 | * System.currentTimeMillis has far less overhead than System.nanoTime. 33 | * @param value wraps a FiniteDuration which represents the duration since the epoch. 34 | */ 35 | private class DefaultImpl(override val value: FiniteDuration) extends Deadline { 36 | def this() = this(System.currentTimeMillis.millis) 37 | override def isOverdue: Boolean = System.currentTimeMillis > value.toMillis 38 | override def +(duration: FiniteDuration): Deadline = new DefaultImpl(value + duration) 39 | override def -(duration: FiniteDuration): Deadline = new DefaultImpl(value - duration) 40 | } 41 | } 42 | 43 | /** 44 | * Mirrors a subset of the scala.concurrent.duration.Deadline api. The motivation is to allow for 45 | * testing where we want to deterministically control the how the test clock evolves over time. 46 | */ 47 | private[nio] trait Deadline extends Comparable[Deadline] { 48 | def isOverdue: Boolean 49 | def value: Duration 50 | def +(duration: FiniteDuration): Deadline 51 | def -(duration: FiniteDuration): Deadline 52 | final def -(deadline: Deadline): Duration = this.value match { 53 | case fd: FiniteDuration => 54 | deadline.value match { 55 | case tfd: FiniteDuration => fd - tfd 56 | case _ => Duration.MinusInf 57 | } 58 | case d => d 59 | } 60 | final def <(that: Deadline): Boolean = this.compareTo(that) < 0 61 | final def <=(that: Deadline): Boolean = this.compareTo(that) <= 0 62 | final def >(that: Deadline): Boolean = this.compareTo(that) > 0 63 | final def >=(that: Deadline): Boolean = this.compareTo(that) >= 0 64 | override def compareTo(that: Deadline): Int = this.value compareTo that.value 65 | } 66 | private[nio] object Deadline { 67 | def now(implicit timeSource: TimeSource): Deadline = timeSource.now 68 | private[nio] object Inf extends Deadline { 69 | override val value: Duration = Duration.Inf 70 | override def isOverdue: Boolean = false 71 | override def compareTo(o: Deadline): Int = this.value compareTo o.value 72 | override def +(duration: FiniteDuration): Deadline = this 73 | override def -(duration: FiniteDuration): Deadline = this 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /io/src/main/scala/sbt/internal/nio/WatchLogger.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * sbt IO 3 | * Copyright Scala Center, Lightbend, and Mark Harrah 4 | * 5 | * Licensed under Apache License 2.0 6 | * SPDX-License-Identifier: Apache-2.0 7 | * 8 | * See the NOTICE file distributed with this work for 9 | * additional information regarding copyright ownership. 10 | */ 11 | 12 | package sbt.internal.nio 13 | 14 | private[sbt] trait WatchLogger { 15 | def debug(msg: Any): Unit 16 | } 17 | private[sbt] object NullWatchLogger extends WatchLogger { 18 | private def ignoreArg[T](f: => T): Unit = 19 | if (false) { 20 | f; () 21 | } else () 22 | override def debug(msg: Any): Unit = ignoreArg(msg) 23 | } 24 | -------------------------------------------------------------------------------- /io/src/main/scala/sbt/internal/nio/WatchServiceBackedObservable.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * sbt IO 3 | * Copyright Scala Center, Lightbend, and Mark Harrah 4 | * 5 | * Licensed under Apache License 2.0 6 | * SPDX-License-Identifier: Apache-2.0 7 | * 8 | * See the NOTICE file distributed with this work for 9 | * additional information regarding copyright ownership. 10 | */ 11 | 12 | package sbt.internal.nio 13 | 14 | import java.io.IOException 15 | import java.nio.file.StandardWatchEventKinds.OVERFLOW 16 | import java.nio.file.{ Path, WatchKey } 17 | import java.util.concurrent.atomic.{ AtomicBoolean, AtomicInteger } 18 | import java.util.concurrent.{ CountDownLatch, TimeUnit } 19 | 20 | import sbt.internal.io._ 21 | import sbt.internal.nio.FileEvent.{ Creation, Deletion } 22 | import sbt.nio.file.FileAttributes.NonExistent 23 | import sbt.nio.file.Glob._ 24 | import sbt.nio.file.{ AnyPath, FileAttributes, FileTreeView, Glob, RecursiveGlob } 25 | 26 | import scala.annotation.tailrec 27 | import scala.collection.JavaConverters._ 28 | import scala.concurrent.duration._ 29 | import scala.util.control.NonFatal 30 | 31 | private[sbt] object WatchServiceBackedObservable { 32 | private val eventThreadId = new AtomicInteger(0) 33 | } 34 | private[sbt] class WatchServiceBackedObservable( 35 | s: NewWatchState, 36 | delay: FiniteDuration, 37 | closeService: Boolean, 38 | logger: WatchLogger 39 | ) extends Registerable[FileEvent[FileAttributes]] 40 | with Observable[FileEvent[FileAttributes]] { 41 | import WatchServiceBackedObservable.eventThreadId 42 | private[this] type Event = FileEvent[FileAttributes] 43 | private[this] val closed = new AtomicBoolean(false) 44 | private[this] val observers = new Observers[FileEvent[FileAttributes]] 45 | private[this] val fileCache = new FileCache(p => FileAttributes(p).getOrElse(NonExistent)) 46 | private[this] val view: FileTreeView.Nio[FileAttributes] = FileTreeView.default 47 | private[this] val thread: Thread = { 48 | val latch = new CountDownLatch(1) 49 | new Thread(s"watch-state-event-thread-${eventThreadId.incrementAndGet()}") { 50 | setDaemon(true) 51 | start() 52 | if (!latch.await(5, TimeUnit.SECONDS)) 53 | throw new IllegalStateException("Couldn't start event thread") 54 | @tailrec 55 | final def loopImpl(): Unit = { 56 | try { 57 | if (!closed.get) getFilesForKey(s.service.poll(delay)).foreach { event => 58 | observers.onNext(event) 59 | } 60 | } catch { 61 | case NonFatal(e) => 62 | logger.debug( 63 | s"Error getting files from ${s.service}: $e\n${e.getStackTrace mkString "\n"}" 64 | ) 65 | } 66 | if (!closed.get) loopImpl() 67 | } 68 | override def run(): Unit = { 69 | latch.countDown() 70 | try { 71 | loopImpl() 72 | } catch { 73 | case _: InterruptedException => 74 | } 75 | } 76 | 77 | def getFilesForKey(key: WatchKey): Seq[Event] = key match { 78 | case null => Vector.empty 79 | case k => 80 | val rawEvents = k.synchronized { 81 | val events = k.pollEvents.asScala.toVector 82 | k.reset() 83 | events 84 | } 85 | val keyPath = k.watchable.asInstanceOf[Path] 86 | val allEvents: Seq[Event] = rawEvents.flatMap { 87 | case e if e.kind.equals(OVERFLOW) => 88 | fileCache.refresh(Glob(keyPath, RecursiveGlob)) 89 | case e if !e.kind.equals(OVERFLOW) && e.context != null => 90 | val path = keyPath.resolve(e.context.asInstanceOf[Path]) 91 | FileAttributes(path) match { 92 | case Right(attrs) => fileCache.update(path, attrs) 93 | case _ => Nil 94 | } 95 | case _ => Nil: Seq[Event] 96 | } 97 | logger.debug(s"Received events:\n${allEvents.mkString("\n")}") 98 | def process(event: Event): Seq[Event] = { 99 | (event match { 100 | case Creation(path, attrs) if attrs.isDirectory => 101 | s.register(path) 102 | event +: view.list(Glob(path, RecursiveGlob)).flatMap { case (p, a) => 103 | process(Creation(p, a)) 104 | } 105 | case Deletion(p, attrs) if attrs.isDirectory => 106 | val events = fileCache.refresh(Glob(p, RecursiveGlob)) 107 | events.view.filter(_.attributes.isDirectory).foreach(e => s.unregister(e.path)) 108 | events 109 | case e => e :: Nil 110 | }).map { 111 | case d @ Deletion(path, attributes) => 112 | val newAttrs = FileAttributes( 113 | isDirectory = attributes.isDirectory, 114 | isOther = false, 115 | isRegularFile = attributes.isRegularFile, 116 | isSymbolicLink = attributes.isSymbolicLink 117 | ) 118 | Deletion(path, newAttrs, d.occurredAt) 119 | case e => e 120 | } 121 | } 122 | allEvents.flatMap(process) 123 | } 124 | 125 | } 126 | } 127 | override def addObserver(observer: Observer[FileEvent[FileAttributes]]): AutoCloseable = 128 | observers.addObserver(observer) 129 | 130 | override def close(): Unit = { 131 | if (closed.compareAndSet(false, true)) { 132 | thread.interrupt() 133 | thread.join(5.seconds.toMillis) 134 | if (closeService) s.service.close() 135 | logger.debug("Closed WatchServiceBackedObservable") 136 | } 137 | } 138 | 139 | override def register(glob: Glob): Either[IOException, Observable[FileEvent[FileAttributes]]] = { 140 | try { 141 | fileCache.register(glob) 142 | val updatedGlob = glob.range._2 match { 143 | case Int.MaxValue => Glob(glob.base, RecursiveGlob) 144 | case d => (1 to d).foldLeft(Glob(glob.base)) { case (g, _) => g / AnyPath } 145 | } 146 | fileCache.list(updatedGlob) foreach { 147 | case (path, attrs) if attrs.isDirectory => s.register(path) 148 | case _ => 149 | } 150 | val observable = new Observers[FileEvent[FileAttributes]] { 151 | override def onNext(t: FileEvent[FileAttributes]): Unit = { 152 | if (glob.matches(t.path)) super.onNext(t) 153 | } 154 | } 155 | val handle = observers.addObserver(observable) 156 | Right(new Observable[FileEvent[FileAttributes]] { 157 | override def addObserver(observer: Observer[FileEvent[FileAttributes]]): AutoCloseable = 158 | observable.addObserver(observer) 159 | override def close(): Unit = handle.close() 160 | override def toString = s"Observable($glob)" 161 | }) 162 | } catch { 163 | case e: IOException => Left(e) 164 | } 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /io/src/main/scala/sbt/io/Hash.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * sbt IO 3 | * Copyright Scala Center, Lightbend, and Mark Harrah 4 | * 5 | * Licensed under Apache License 2.0 6 | * SPDX-License-Identifier: Apache-2.0 7 | * 8 | * See the NOTICE file distributed with this work for 9 | * additional information regarding copyright ownership. 10 | */ 11 | 12 | package sbt.io 13 | 14 | import java.io._ 15 | import java.net.{ URI, URL } 16 | 17 | object Hash { 18 | private val BufferSize = 8192 19 | 20 | /** Converts an array of `bytes` to a hexadecimal representation String. */ 21 | def toHex(bytes: Array[Byte]): String = { 22 | val buffer = new StringBuilder(bytes.length * 2) 23 | for (i <- bytes.indices) { 24 | val b = bytes(i) 25 | val bi: Int = if (b < 0) b + 256 else b.toInt 26 | buffer append toHex((bi >>> 4).asInstanceOf[Byte]) 27 | buffer append toHex((bi & 0x0f).asInstanceOf[Byte]) 28 | } 29 | buffer.toString 30 | } 31 | 32 | /** 33 | * Converts the provided hexadecimal representation `hex` to an array of bytes. 34 | * The hexadecimal representation must have an even number of characters in the range 0-9, a-f, or A-F. 35 | */ 36 | def fromHex(hex: String): Array[Byte] = { 37 | require((hex.length & 1) == 0, "Hex string must have length 2n.") 38 | val array = new Array[Byte](hex.length >> 1) 39 | for (i <- 0 until hex.length by 2) { 40 | val c1 = hex.charAt(i) 41 | val c2 = hex.charAt(i + 1) 42 | array(i >> 1) = ((fromHex(c1) << 4) | fromHex(c2)).asInstanceOf[Byte] 43 | } 44 | array 45 | } 46 | 47 | /** Truncates the last half of `s` if the string has at least four characters. Otherwise, the original string is returned. */ 48 | def halve(s: String): String = if (s.length > 3) s.substring(0, s.length / 2) else s 49 | 50 | /** Computes the SHA-1 hash of `s` and returns the first `i` characters of the hexadecimal representation of the hash. */ 51 | def trimHashString(s: String, i: Int): String = toHex(apply(s)).take(i) 52 | 53 | /** Computes the SHA-1 hash of `s` and truncates the hexadecimal representation of the hash via [[halve]]. */ 54 | def halfHashString(s: String): String = halve(toHex(apply(s))) 55 | 56 | /** Calculates the SHA-1 hash of the given String. */ 57 | def apply(s: String): Array[Byte] = apply(s.getBytes("UTF-8")) 58 | 59 | /** Calculates the SHA-1 hash of the given Array[Byte]. */ 60 | def apply(as: Array[Byte]): Array[Byte] = apply(new ByteArrayInputStream(as)) 61 | 62 | /** Calculates the SHA-1 hash of the given file. */ 63 | def apply(file: File): Array[Byte] = 64 | try apply(new BufferedInputStream(new FileInputStream(file))) // apply closes the stream 65 | catch { case _: FileNotFoundException => apply("") } 66 | 67 | /** Calculates the SHA-1 hash of the given resource. */ 68 | def apply(url: URL): Array[Byte] = Using.urlInputStream(url)(apply) 69 | 70 | /** 71 | * If the URI represents a local file (the scheme is "file"), 72 | * this method calculates the SHA-1 hash of the contents of that file. 73 | * Otherwise, this methods calculates the SHA-1 hash of the normalized string representation of the URI. 74 | */ 75 | def contentsIfLocal(uri: URI): Array[Byte] = 76 | if (uri.getScheme == "file") apply(uri.toURL) else apply(uri.normalize.toString) 77 | 78 | /** Calculates the SHA-1 hash of the given stream, closing it when finished. */ 79 | def apply(stream: InputStream): Array[Byte] = { 80 | import java.security.{ DigestInputStream, MessageDigest } 81 | val digest = MessageDigest.getInstance("SHA") 82 | try { 83 | val dis = new DigestInputStream(stream, digest) 84 | val buffer = new Array[Byte](BufferSize) 85 | while (dis.read(buffer) >= 0) {} 86 | dis.close() 87 | digest.digest 88 | } finally { 89 | stream.close() 90 | } 91 | } 92 | 93 | private def toHex(b: Byte): Char = { 94 | require(b >= 0 && b <= 15, "Byte " + b + " was not between 0 and 15") 95 | if (b < 10) 96 | ('0'.asInstanceOf[Int] + b).asInstanceOf[Char] 97 | else 98 | ('a'.asInstanceOf[Int] + (b - 10)).asInstanceOf[Char] 99 | } 100 | 101 | private def fromHex(c: Char): Int = { 102 | val b = 103 | if (c >= '0' && c <= '9') 104 | (c - '0') 105 | else if (c >= 'a' && c <= 'f') 106 | (c - 'a') + 10 107 | else if (c >= 'A' && c <= 'F') 108 | (c - 'A') + 10 109 | else 110 | throw new RuntimeException("Invalid hex character: '" + c + "'.") 111 | b 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /io/src/main/scala/sbt/io/JavaMilli.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * sbt IO 3 | * Copyright Scala Center, Lightbend, and Mark Harrah 4 | * 5 | * Licensed under Apache License 2.0 6 | * SPDX-License-Identifier: Apache-2.0 7 | * 8 | * See the NOTICE file distributed with this work for 9 | * additional information regarding copyright ownership. 10 | */ 11 | 12 | package sbt.io 13 | 14 | import java.io.FileNotFoundException 15 | import java.nio.file.attribute.FileTime 16 | import java.nio.file.{ Files, NoSuchFileException, Paths => JPaths } 17 | 18 | import sbt.internal.io.MilliMilliseconds 19 | 20 | object JavaMilli extends MilliMilliseconds { 21 | def getModifiedTime(filePath: String): Long = 22 | mapNoSuchFileException(Files.getLastModifiedTime(JPaths.get(filePath)).toMillis) 23 | 24 | def setModifiedTime(filePath: String, mtime: Long): Unit = 25 | mapNoSuchFileException { 26 | Files.setLastModifiedTime(JPaths.get(filePath), FileTime.fromMillis(mtime)) 27 | () 28 | } 29 | 30 | private def mapNoSuchFileException[A](f: => A): A = 31 | try { 32 | f 33 | } catch { 34 | case e: NoSuchFileException => throw new FileNotFoundException(e.getFile).initCause(e) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /io/src/main/scala/sbt/io/PathMapper.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * sbt IO 3 | * Copyright Scala Center, Lightbend, and Mark Harrah 4 | * 5 | * Licensed under Apache License 2.0 6 | * SPDX-License-Identifier: Apache-2.0 7 | * 8 | * See the NOTICE file distributed with this work for 9 | * additional information regarding copyright ownership. 10 | */ 11 | 12 | package sbt.io 13 | 14 | import java.io.File 15 | 16 | abstract class Mapper { 17 | type PathMap = File => Option[String] 18 | type FileMap = File => Option[File] 19 | 20 | /** A path mapper that pairs a File with the path returned by calling `getPath` on it. */ 21 | val basic: PathMap = f => Some(f.getPath) 22 | 23 | /** 24 | * A path mapper that pairs a File with its path relative to `base`. 25 | * If the File is not a descendant of `base`, it is not handled (None is returned by the mapper). 26 | */ 27 | def relativeTo(base: File): PathMap = IO.relativize(base, _) 28 | 29 | def relativeTo(bases: Iterable[File], zero: PathMap = transparent): PathMap = 30 | fold(zero, bases)(relativeTo) 31 | 32 | /** 33 | * A path mapper that pairs a descendent of `oldBase` with `newBase` prepended to the path relative to `oldBase`. 34 | * For example, if `oldBase = /old/x/` and `newBase = new/a/`, then `/old/x/y/z.txt` gets paired with `new/a/y/z.txt`. 35 | */ 36 | def rebase(oldBase: File, newBase: String): PathMap = { 37 | val normNewBase = normalizeBase(newBase) 38 | (file: File) => 39 | if (file == oldBase) 40 | Some(if (normNewBase.isEmpty) "." else normNewBase) 41 | else 42 | IO.relativize(oldBase, file).map(normNewBase + _) 43 | } 44 | 45 | /** 46 | * A mapper that throws an exception for any input. 47 | * This is useful as the last mapper in a pipeline to ensure every input gets mapped. 48 | */ 49 | def fail: Any => Nothing = f => sys.error("No mapping for " + f) 50 | 51 | /** A path mapper that pairs a File with its name. For example, `/x/y/z.txt` gets paired with `z.txt`. */ 52 | val flat: PathMap = f => Some(f.getName) 53 | 54 | /** 55 | * A path mapper that pairs a File with a path constructed from `newBase` and the file's name. 56 | * For example, if `newBase = /new/a/`, then `/old/x/z.txt` gets paired with `/new/a/z.txt`. 57 | */ 58 | def flatRebase(newBase: String): PathMap = { 59 | val newBase0 = normalizeBase(newBase) 60 | (f => Some(newBase0 + f.getName)) 61 | } 62 | 63 | /** A mapper that is defined on all inputs by the function `f`. */ 64 | def total[A, B](f: A => B): A => Some[B] = x => Some(f(x)) 65 | 66 | /** A mapper that ignores all inputs. */ 67 | def transparent: Any => Option[Nothing] = _ => None 68 | 69 | def normalizeBase(base: String) = if (!base.isEmpty && !base.endsWith("/")) base + "/" else base 70 | 71 | /** 72 | * Pairs a File with the absolute File obtained by calling `getAbsoluteFile`. 73 | * Note that this usually means that relative files are resolved against the current working directory. 74 | */ 75 | def abs: FileMap = f => Some(f.getAbsoluteFile) 76 | 77 | /** 78 | * Returns a FileMap that resolves a relative File against `newDirectory` 79 | * and pairs the original File with the resolved File. 80 | * The mapper ignores absolute files. 81 | */ 82 | def resolve(newDirectory: File): FileMap = 83 | file => if (file.isAbsolute) None else Some(new File(newDirectory, file.getPath)) 84 | 85 | def rebase(oldBases: Iterable[File], newBase: File, zero: FileMap = transparent): FileMap = 86 | fold(zero, oldBases)(old => rebase(old, newBase)) 87 | 88 | /** 89 | * Produces a File mapper that pairs a descendant of `oldBase` with a file in `newBase` that preserving the relative path of the original file against `oldBase`. 90 | * For example, if `oldBase` is `/old/x/` and `newBase` is `/new/a/`, `/old/x/y/z.txt` gets paired with `/new/a/y/z.txt`. 91 | */ 92 | def rebase(oldBase: File, newBase: File): FileMap = 93 | file => 94 | if (file == oldBase) 95 | Some(newBase) 96 | else 97 | IO.relativize(oldBase, file) map (r => new File(newBase, r)) 98 | 99 | /** 100 | * Constructs a FileMap that pairs a file with a file with the same name in `newDirectory`. 101 | * For example, if `newDirectory` is `/a/b`, then `/r/s/t/d.txt` will be paired with `/a/b/d.txt` 102 | */ 103 | def flat(newDirectory: File): FileMap = file => Some(new File(newDirectory, file.getName)) 104 | 105 | /** 106 | * Selects all descendants of `base` directory and maps them to a path relative to `base`. 107 | * `base` itself is not included. 108 | */ 109 | def allSubpaths(base: File): Traversable[(File, String)] = 110 | selectSubpaths(base, AllPassFilter) 111 | 112 | /** 113 | * Selects descendants of `base` directory matching `filter` and maps them to a path relative to `base`. 114 | * `base` itself is not included. 115 | */ 116 | def selectSubpaths(base: File, filter: FileFilter): Traversable[(File, String)] = 117 | PathFinder(base).globRecursive(filter).get().collect { 118 | case f if f != base => f -> base.toPath.relativize(f.toPath).toString 119 | } 120 | 121 | /** 122 | * return a Seq of mappings which effect is to add a whole directory in the generated package 123 | * 124 | * @example In order to create mappings for a static directory "extra" add 125 | * {{{ 126 | * mappings ++= directory(baseDirectory.value / "extra") 127 | * }}} 128 | * 129 | * The resulting mappings sequence will look something like this 130 | * 131 | * {{{ 132 | * File(baseDirectory/extras) -> "extras" 133 | * File(baseDirectory/extras/file1) -> "extras/file1" 134 | * File(baseDirectory/extras/file2) -> "extras/file2" 135 | * ... 136 | * }}} 137 | * 138 | * @param baseDirectory The directory that should be turned into a mappings sequence. 139 | * @return mappings The `baseDirectory` and all of its contents 140 | */ 141 | def directory(baseDirectory: File): Seq[(File, String)] = 142 | Option(baseDirectory.getParentFile) 143 | .map(parent => PathFinder(baseDirectory).allPaths pair relativeTo(parent)) 144 | .getOrElse(PathFinder(baseDirectory).allPaths pair basic) 145 | 146 | /** 147 | * return a Seq of mappings excluding the directory itself. 148 | * 149 | * @example In order to create mappings for a static directory "extra" add 150 | * {{{ 151 | * mappings ++= contentOf(baseDirectory.value / "extra") 152 | * }}} 153 | * 154 | * The resulting mappings sequence will look something like this 155 | * 156 | * {{{ 157 | * File(baseDirectory/extras/file1) -> "file1" 158 | * File(baseDirectory/extras/file2) -> "file2" 159 | * ... 160 | * }}} 161 | * 162 | * @example Add a static directory "extra" and re-map the destination to a different path 163 | * {{{ 164 | * mappings ++= contentOf(baseDirectory.value / "extra").map { 165 | * case (src, destination) => src -> s"new/path/destination" 166 | * } 167 | * }}} 168 | * 169 | * @param baseDirectory The directory that should be turned into a mappings sequence. 170 | * @return mappings - The `basicDirectory`'s contents exlcuding `basicDirectory` itself 171 | */ 172 | def contentOf(baseDirectory: File): Seq[(File, String)] = ( 173 | (PathFinder(baseDirectory).allPaths --- PathFinder(baseDirectory)) 174 | pair relativeTo(baseDirectory) 175 | ) 176 | 177 | private[this] def fold[A, B, T](zero: A => Option[B], in: Iterable[T])( 178 | f: T => A => Option[B] 179 | ): A => Option[B] = 180 | in.foldLeft(zero)((mapper, base) => a => f(base)(a) orElse mapper(a)) 181 | } 182 | -------------------------------------------------------------------------------- /io/src/main/scala/sbt/io/Using.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * sbt IO 3 | * Copyright Scala Center, Lightbend, and Mark Harrah 4 | * 5 | * Licensed under Apache License 2.0 6 | * SPDX-License-Identifier: Apache-2.0 7 | * 8 | * See the NOTICE file distributed with this work for 9 | * additional information regarding copyright ownership. 10 | */ 11 | 12 | package sbt 13 | package io 14 | 15 | import java.io.{ 16 | BufferedInputStream, 17 | BufferedOutputStream, 18 | BufferedReader, 19 | BufferedWriter, 20 | File, 21 | FileInputStream, 22 | FileOutputStream, 23 | IOException, 24 | InputStream, 25 | InputStreamReader, 26 | OutputStream, 27 | OutputStreamWriter, 28 | } 29 | import java.net.URL 30 | import java.nio.charset.Charset 31 | import java.util.jar.{ JarFile, JarInputStream, JarOutputStream } 32 | import java.util.zip.{ GZIPInputStream, _ } 33 | 34 | import sbt.internal.io.ErrorHandling.translate 35 | 36 | abstract class Using[Source, T] { 37 | protected def open(src: Source): T 38 | def apply[R](src: Source)(f: T => R): R = { 39 | val resource = open(src) 40 | try { 41 | f(resource) 42 | } finally { 43 | close(resource) 44 | } 45 | } 46 | protected def close(out: T): Unit 47 | } 48 | 49 | import scala.reflect.{ Manifest => SManifest } 50 | private[sbt] abstract class WrapUsing[Source, T](implicit 51 | srcMf: SManifest[Source], 52 | targetMf: SManifest[T] 53 | ) extends Using[Source, T] { 54 | protected def label[S](m: SManifest[S]) = m.runtimeClass.getSimpleName 55 | protected def openImpl(source: Source): T 56 | protected final def open(source: Source): T = 57 | translate("Error wrapping " + label(srcMf) + " in " + label(targetMf) + ": ")(openImpl(source)) 58 | } 59 | private[sbt] trait OpenFile[T] extends Using[File, T] { 60 | protected def openImpl(file: File): T 61 | protected final def open(file: File): T = { 62 | val parent = file.getParentFile 63 | if (parent != null) { 64 | try IO.createDirectory(parent) 65 | catch { case _: IOException => } 66 | } 67 | openImpl(file) 68 | } 69 | } 70 | 71 | object Using { 72 | def wrap[Source, T <: AutoCloseable](openF: Source => T)(implicit 73 | srcMf: SManifest[Source], 74 | targetMf: SManifest[T] 75 | ): Using[Source, T] = 76 | wrap(openF, closeCloseable) 77 | 78 | def wrap[Source, T](openF: Source => T, closeF: T => Unit)(implicit 79 | srcMf: SManifest[Source], 80 | targetMf: SManifest[T] 81 | ): Using[Source, T] = 82 | new WrapUsing[Source, T] { 83 | def openImpl(source: Source) = openF(source) 84 | def close(t: T) = closeF(t) 85 | } 86 | 87 | def resource[Source, T <: AutoCloseable](openF: Source => T): Using[Source, T] = 88 | resource(openF, closeCloseable) 89 | 90 | def resource[Source, T](openF: Source => T, closeF: T => Unit): Using[Source, T] = 91 | new Using[Source, T] { 92 | def open(s: Source) = openF(s) 93 | def close(s: T) = closeF(s) 94 | } 95 | 96 | def file[T <: AutoCloseable](openF: File => T): OpenFile[T] = file(openF, closeCloseable) 97 | 98 | def file[T](openF: File => T, closeF: T => Unit): OpenFile[T] = 99 | new OpenFile[T] { 100 | def openImpl(file: File) = openF(file) 101 | def close(t: T) = closeF(t) 102 | } 103 | 104 | private def closeCloseable[T <: AutoCloseable]: T => Unit = _.close() 105 | 106 | val bufferedOutputStream = wrap((out: OutputStream) => new BufferedOutputStream(out)) 107 | val bufferedInputStream = wrap((in: InputStream) => new BufferedInputStream(in)) 108 | 109 | def fileOutputStream(append: Boolean = false) = 110 | file(f => new BufferedOutputStream(new FileOutputStream(f, append))) 111 | 112 | val fileInputStream = file(f => new BufferedInputStream(new FileInputStream(f))) 113 | 114 | val urlInputStream = resource((u: URL) => 115 | translate("Error opening " + u + ": ")(new BufferedInputStream(u.openStream)) 116 | ) 117 | 118 | val fileOutputChannel = file(f => new FileOutputStream(f).getChannel) 119 | val fileInputChannel = file(f => new FileInputStream(f).getChannel) 120 | 121 | def fileWriter(charset: Charset = IO.utf8, append: Boolean = false) = 122 | file(f => new BufferedWriter(new OutputStreamWriter(new FileOutputStream(f, append), charset))) 123 | 124 | def fileReader(charset: Charset) = 125 | file(f => new BufferedReader(new InputStreamReader(new FileInputStream(f), charset))) 126 | 127 | def urlReader(charset: Charset) = 128 | resource((u: URL) => new BufferedReader(new InputStreamReader(u.openStream, charset))) 129 | 130 | def jarFile(verify: Boolean) = file(f => new JarFile(f, verify), (_: JarFile).close()) 131 | val zipFile = file(f => new ZipFile(f), (_: ZipFile).close()) 132 | 133 | val streamReader = wrap { 134 | (_: (InputStream, Charset)) match { case (in, charset) => new InputStreamReader(in, charset) } 135 | } 136 | 137 | val gzipInputStream = wrap((in: InputStream) => new GZIPInputStream(in, 8192)) 138 | val zipInputStream = wrap((in: InputStream) => new ZipInputStream(in)) 139 | val zipOutputStream = wrap((out: OutputStream) => new ZipOutputStream(out)) 140 | 141 | val gzipOutputStream = 142 | wrap((out: OutputStream) => new GZIPOutputStream(out, 8192), (_: GZIPOutputStream).finish()) 143 | 144 | val jarOutputStream = wrap((out: OutputStream) => new JarOutputStream(out)) 145 | val jarInputStream = wrap((in: InputStream) => new JarInputStream(in)) 146 | 147 | def zipEntry(zip: ZipFile) = 148 | resource((entry: ZipEntry) => 149 | translate("Error opening " + entry.getName + " in " + zip + ": ") { 150 | zip.getInputStream(entry) 151 | } 152 | ) 153 | } 154 | -------------------------------------------------------------------------------- /io/src/main/scala/sbt/io/WatchService.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * sbt IO 3 | * Copyright Scala Center, Lightbend, and Mark Harrah 4 | * 5 | * Licensed under Apache License 2.0 6 | * SPDX-License-Identifier: Apache-2.0 7 | * 8 | * See the NOTICE file distributed with this work for 9 | * additional information regarding copyright ownership. 10 | */ 11 | 12 | package sbt.io 13 | 14 | import java.nio.file.{ 15 | ClosedWatchServiceException, 16 | FileSystems, 17 | WatchEvent, 18 | WatchKey, 19 | Path => JPath, 20 | WatchService => JWatchService 21 | } 22 | import java.util.concurrent.TimeUnit 23 | 24 | import sbt.internal.nio 25 | 26 | import scala.annotation.tailrec 27 | import scala.collection.JavaConverters._ 28 | import scala.collection.{ immutable, mutable } 29 | import scala.concurrent.duration.{ Duration, FiniteDuration } 30 | import scala.util.Properties 31 | 32 | object WatchService { 33 | 34 | /** 35 | * Adapts a Java `WatchService` to be used with sbt's `WatchService` infrastructure. 36 | * @param service The `WatchService` to use. 37 | */ 38 | implicit final class WatchServiceAdapter(service: JWatchService) 39 | extends WatchService 40 | with Unregisterable { 41 | private var closed: Boolean = false 42 | private val registered: mutable.Map[JPath, WatchKey] = mutable.Map.empty 43 | 44 | override def init(): Unit = 45 | () 46 | 47 | override def pollEvents(): Map[WatchKey, immutable.Seq[WatchEvent[JPath]]] = { 48 | val values = registered.synchronized(registered.values.toIndexedSeq) 49 | values.flatMap { k => 50 | val events = k.pollEvents() 51 | if (events.isEmpty) None 52 | else Some((k, events.asScala.asInstanceOf[Seq[WatchEvent[JPath]]].toIndexedSeq)) 53 | }.toMap 54 | } 55 | 56 | @tailrec 57 | override def poll(timeout: Duration): WatchKey = 58 | if (timeout.isFinite) { 59 | service.poll(timeout.toMillis, TimeUnit.MILLISECONDS) 60 | } else { 61 | service.poll(1000L, TimeUnit.MILLISECONDS) match { 62 | case null => poll(timeout) 63 | case key => key 64 | } 65 | } 66 | 67 | override def register(path: JPath, events: WatchEvent.Kind[JPath]*): WatchKey = { 68 | if (closed) throw new ClosedWatchServiceException 69 | else { 70 | registered.synchronized { 71 | registered.get(path) match { 72 | case None => 73 | val key = path.register(service, events*) 74 | registered += path -> key 75 | key 76 | case Some(key) => 77 | key 78 | } 79 | } 80 | } 81 | } 82 | 83 | override def unregister(path: JPath): Unit = { 84 | if (closed) throw new ClosedWatchServiceException 85 | registered.synchronized { 86 | registered.get(path) match { 87 | case Some(key) => 88 | key.cancel() 89 | registered -= path 90 | case _ => 91 | } 92 | } 93 | () 94 | } 95 | 96 | override def close(): Unit = { 97 | closed = true 98 | service.close() 99 | } 100 | 101 | override def toString: String = service.toString 102 | } 103 | private[sbt] def default: WatchService = 104 | if (Properties.isMac) new MacOSXWatchService else FileSystems.getDefault.newWatchService 105 | def polling(delay: FiniteDuration): WatchService = new PollingWatchService(delay) 106 | } 107 | 108 | /** 109 | * A service that will monitor the file system for file creation, deletion 110 | * and modification. 111 | */ 112 | trait WatchService { 113 | 114 | /** Initializes the watchservice. */ 115 | def init(): Unit 116 | 117 | /** 118 | * Retrieves all the events and groups them by watch key. 119 | * Does not wait if no event is available. 120 | * @return The pending events. 121 | */ 122 | def pollEvents(): Map[WatchKey, immutable.Seq[WatchEvent[JPath]]] 123 | 124 | /** 125 | * Retrieves the next `WatchKey` that has a `WatchEvent` waiting. Waits 126 | * until the `timeout` is expired is no such key exists. 127 | * @param timeout Maximum time to wait 128 | * @return The next `WatchKey` that received an event, or null if no such 129 | * key exists. 130 | */ 131 | def poll(timeout: Duration): WatchKey 132 | 133 | /** 134 | * Registers a path to be monitored. 135 | * @param path The path to monitor. 136 | * @param events The events that should be registered. 137 | * @return A `WatchKey`, that represents a token of registration. 138 | */ 139 | def register(path: JPath, events: WatchEvent.Kind[JPath]*): WatchKey 140 | 141 | /** 142 | * Closes this `WatchService`. 143 | */ 144 | def close(): Unit 145 | } 146 | 147 | private[sbt] trait Unregisterable { self: WatchService => 148 | 149 | /** 150 | * Unregisters a monitored path. 151 | * @param path The monitored path. 152 | */ 153 | def unregister(path: JPath): Unit 154 | } 155 | 156 | private[sbt] class MacOSXWatchService extends sbt.internal.io.MacOSXWatchService 157 | private[sbt] class PollingWatchService(delay: FiniteDuration) extends nio.PollingWatchService(delay) 158 | -------------------------------------------------------------------------------- /io/src/main/scala/sbt/io/syntax.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * sbt IO 3 | * Copyright Scala Center, Lightbend, and Mark Harrah 4 | * 5 | * Licensed under Apache License 2.0 6 | * SPDX-License-Identifier: Apache-2.0 7 | * 8 | * See the NOTICE file distributed with this work for 9 | * additional information regarding copyright ownership. 10 | */ 11 | 12 | package sbt.io 13 | 14 | import java.io.File 15 | 16 | @deprecated("Alternative is likely to be removed in future versions of sbt", "1.3.0") 17 | private[sbt] trait Alternative[A, B] { 18 | def |(g: A => Option[B]): A => Option[B] 19 | } 20 | 21 | sealed trait BaseSyntax { 22 | implicit def singleFileFinder(file: File): PathFinder = PathFinder(file) 23 | } 24 | sealed abstract class IOSyntax1 extends BaseSyntax 25 | 26 | sealed abstract class IOSyntax0 extends IOSyntax1 { 27 | @deprecated("Alternative is no longer used in sbt io.", "1.3.0") 28 | implicit def alternative[A, B](f: A => Option[B]): Alternative[A, B] = g => a => f(a) orElse g(a) 29 | } 30 | 31 | private[sbt] trait IOSyntax extends BaseSyntax 32 | 33 | object syntax extends IOSyntax0 { 34 | type File = java.io.File 35 | type URI = java.net.URI 36 | type URL = java.net.URL 37 | 38 | def uri(s: String): URI = new URI(s) 39 | def file(s: String): File = new File(s) 40 | def url(s: String): URL = uri(s).toURL 41 | 42 | implicit def fileToRichFile(file: File): RichFile = new RichFile(file) 43 | implicit def filesToFinder(cc: Traversable[File]): PathFinder = PathFinder.strict(cc) 44 | } 45 | -------------------------------------------------------------------------------- /io/src/main/scala/sbt/nio/file/ChangedFiles.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * sbt IO 3 | * Copyright Scala Center, Lightbend, and Mark Harrah 4 | * 5 | * Licensed under Apache License 2.0 6 | * SPDX-License-Identifier: Apache-2.0 7 | * 8 | * See the NOTICE file distributed with this work for 9 | * additional information regarding copyright ownership. 10 | */ 11 | 12 | package sbt.nio.file 13 | 14 | import java.nio.file.Path 15 | 16 | /** 17 | * Represents a set of possible file changes. 18 | * 19 | * @param created the files that are newly created 20 | * @param deleted the files that have been deleted 21 | * @param updated the files that have been updated 22 | */ 23 | final case class ChangedFiles(created: Seq[Path], deleted: Seq[Path], updated: Seq[Path]) 24 | -------------------------------------------------------------------------------- /io/src/main/scala/sbt/nio/file/FileAttributes.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * sbt IO 3 | * Copyright Scala Center, Lightbend, and Mark Harrah 4 | * 5 | * Licensed under Apache License 2.0 6 | * SPDX-License-Identifier: Apache-2.0 7 | * 8 | * See the NOTICE file distributed with this work for 9 | * additional information regarding copyright ownership. 10 | */ 11 | 12 | package sbt.nio.file 13 | 14 | import java.io.IOException 15 | import java.nio.file.attribute.BasicFileAttributes 16 | import java.nio.file.{ Files, LinkOption, NoSuchFileException, Path => NioPath } 17 | 18 | /** 19 | * Represents a minimal set of attributes a file. In contrast to 20 | * `java.nio.file.attribute.BasicFileAttributes`, it is possible to compute the values provided 21 | * by this trait without stating the file. If all of the methods return false, the user may infer 22 | * that the file to which these attributes corresponds does not exist. An instance of this class 23 | * may not represent that current state of the file if the underlying file has been modified since 24 | * the instance was first created. 25 | */ 26 | sealed trait FileAttributes { 27 | 28 | /** 29 | * Returns true if the underlying file is a regular file. 30 | * @return true if the underlying file is a regular file. 31 | */ 32 | def isRegularFile: Boolean 33 | 34 | /** 35 | * Returns true if the underlying file is a directory. 36 | * @return true if the underlying file is a directory. 37 | */ 38 | def isDirectory: Boolean 39 | 40 | /** 41 | * Returns true if the underlying file is a symbolic link. 42 | * @return true if the underlying file is a symbolic link. 43 | */ 44 | def isSymbolicLink: Boolean 45 | 46 | /** 47 | * Returns true if the underlying file is not a regular file, directory or symbolic link. The 48 | * type of this file is thus platform dependent. For example, on linux, it could be a named 49 | * pipe. 50 | * @return true if the underlying file is not a regular file, directory or symbolic link. 51 | */ 52 | def isOther: Boolean 53 | } 54 | 55 | object FileAttributes { 56 | case object NonExistent extends FileAttributes { 57 | override def isRegularFile: Boolean = false 58 | override def isDirectory: Boolean = false 59 | override def isSymbolicLink: Boolean = false 60 | override def isOther: Boolean = false 61 | } 62 | private final class FileAttributesImpl( 63 | override val isDirectory: Boolean, 64 | override val isOther: Boolean, 65 | override val isRegularFile: Boolean, 66 | override val isSymbolicLink: Boolean 67 | ) extends FileAttributes { 68 | override def hashCode: Int = 69 | (((isRegularFile.hashCode * 31) ^ isDirectory.hashCode) * 31) ^ isSymbolicLink.hashCode 70 | override def equals(o: Any): Boolean = o match { 71 | case that: FileAttributesImpl => 72 | this.isDirectory == that.isDirectory && 73 | this.isOther == that.isOther && 74 | this.isRegularFile == that.isRegularFile && 75 | this.isSymbolicLink == that.isSymbolicLink 76 | case _ => false 77 | } 78 | override def toString: String = 79 | s"FileAttributes(isDirectory = $isDirectory, isOther = $isOther," + 80 | s"isRegularFile = $isRegularFile, isSymbolicLink = $isSymbolicLink)" 81 | } 82 | def apply(path: NioPath): Either[IOException, FileAttributes] = apply(path, followLinks = true) 83 | private[sbt] def apply(path: NioPath, followLinks: Boolean): Either[IOException, FileAttributes] = 84 | try { 85 | val attrs = 86 | Files.readAttributes(path, classOf[BasicFileAttributes], LinkOption.NOFOLLOW_LINKS) 87 | if (attrs.isSymbolicLink && followLinks) { 88 | try { 89 | val linkAttrs = Files.readAttributes(path, classOf[BasicFileAttributes]) 90 | Right( 91 | apply( 92 | linkAttrs.isDirectory, 93 | linkAttrs.isOther, 94 | linkAttrs.isRegularFile, 95 | isSymbolicLink = true 96 | ) 97 | ) 98 | } catch { 99 | case _: NoSuchFileException => 100 | Right( 101 | apply( 102 | isDirectory = false, 103 | isOther = false, 104 | isRegularFile = false, 105 | isSymbolicLink = true 106 | ) 107 | ) 108 | } 109 | } else 110 | Right(apply(attrs.isDirectory, attrs.isOther, attrs.isRegularFile, attrs.isSymbolicLink)) 111 | } catch { 112 | case _: NoSuchFileException => Right(NonExistent) 113 | case e: IOException => Left(e) 114 | } 115 | def apply( 116 | isDirectory: Boolean, 117 | isOther: Boolean, 118 | isRegularFile: Boolean, 119 | isSymbolicLink: Boolean 120 | ): FileAttributes = 121 | new FileAttributesImpl(isDirectory, isOther, isRegularFile, isSymbolicLink) 122 | } 123 | -------------------------------------------------------------------------------- /io/src/main/scala/sbt/nio/file/package.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * sbt IO 3 | * Copyright Scala Center, Lightbend, and Mark Harrah 4 | * 5 | * Licensed under Apache License 2.0 6 | * SPDX-License-Identifier: Apache-2.0 7 | * 8 | * See the NOTICE file distributed with this work for 9 | * additional information regarding copyright ownership. 10 | */ 11 | 12 | package sbt.nio 13 | 14 | package object file { 15 | val * = sbt.nio.file.RelativeGlob.* 16 | val ** = sbt.nio.file.RelativeGlob.** 17 | } 18 | -------------------------------------------------------------------------------- /io/src/main/scala/sbt/nio/file/syntax/package.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * sbt IO 3 | * Copyright Scala Center, Lightbend, and Mark Harrah 4 | * 5 | * Licensed under Apache License 2.0 6 | * SPDX-License-Identifier: Apache-2.0 7 | * 8 | * See the NOTICE file distributed with this work for 9 | * additional information regarding copyright ownership. 10 | */ 11 | 12 | package sbt.nio.file 13 | 14 | import java.io.File 15 | import java.nio.file.Path 16 | 17 | import sbt.nio.file.Glob.{ FileOps, PathOps } 18 | 19 | package object syntax extends syntax0 20 | private[sbt] trait syntax0 { 21 | implicit def pathToPathOps(path: Path): PathOps = new PathOps(path) 22 | implicit def fileToFileOps(file: File): FileOps = new FileOps(file) 23 | } 24 | -------------------------------------------------------------------------------- /io/src/test/resources/zip-slip.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sbt/io/32ef9461b0f5124241c5877075c2994cb841decf/io/src/test/resources/zip-slip.zip -------------------------------------------------------------------------------- /io/src/test/scala/sbt/internal/io/DefaultWatchServiceSpec.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * sbt IO 3 | * Copyright Scala Center, Lightbend, and Mark Harrah 4 | * 5 | * Licensed under Apache License 2.0 6 | * SPDX-License-Identifier: Apache-2.0 7 | * 8 | * See the NOTICE file distributed with this work for 9 | * additional information regarding copyright ownership. 10 | */ 11 | 12 | package sbt.internal.io 13 | 14 | import java.nio.file.FileSystems 15 | 16 | import scala.concurrent.duration._ 17 | import scala.util.Properties 18 | 19 | object DefaultWatchServiceSpec { 20 | val pollDelay = 100.milliseconds 21 | } 22 | 23 | class DefaultWatchServiceSpec 24 | extends SourceModificationWatchSpec( 25 | _ => if (Properties.isMac) new MacOSXWatchService else FileSystems.getDefault.newWatchService, 26 | DefaultWatchServiceSpec.pollDelay 27 | ) 28 | -------------------------------------------------------------------------------- /io/src/test/scala/sbt/internal/io/JavaMilliSpec.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * sbt IO 3 | * Copyright Scala Center, Lightbend, and Mark Harrah 4 | * 5 | * Licensed under Apache License 2.0 6 | * SPDX-License-Identifier: Apache-2.0 7 | * 8 | * See the NOTICE file distributed with this work for 9 | * additional information regarding copyright ownership. 10 | */ 11 | 12 | package sbt.io 13 | 14 | import org.scalatest.flatspec.AnyFlatSpec 15 | 16 | final class JavaMilliSpec extends AnyFlatSpec { 17 | "JavaMilli" should "be exposed to the sbt.io package" in 18 | assertCompiles("""sbt.io.JavaMilli.getModifiedTime("/tmp")""") 19 | } 20 | -------------------------------------------------------------------------------- /io/src/test/scala/sbt/internal/io/PollingWatchServiceSpec.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * sbt IO 3 | * Copyright Scala Center, Lightbend, and Mark Harrah 4 | * 5 | * Licensed under Apache License 2.0 6 | * SPDX-License-Identifier: Apache-2.0 7 | * 8 | * See the NOTICE file distributed with this work for 9 | * additional information regarding copyright ownership. 10 | */ 11 | 12 | package sbt.internal.io 13 | 14 | import sbt.internal.nio.PollingWatchService 15 | 16 | import scala.concurrent.duration._ 17 | 18 | class PollingWatchServiceSpec 19 | extends SourceModificationWatchSpec( 20 | (d: FiniteDuration) => new PollingWatchService(d), 21 | DefaultWatchServiceSpec.pollDelay 22 | ) 23 | -------------------------------------------------------------------------------- /io/src/test/scala/sbt/internal/io/RetrySpec.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * sbt IO 3 | * Copyright Scala Center, Lightbend, and Mark Harrah 4 | * 5 | * Licensed under Apache License 2.0 6 | * SPDX-License-Identifier: Apache-2.0 7 | * 8 | * See the NOTICE file distributed with this work for 9 | * additional information regarding copyright ownership. 10 | */ 11 | 12 | package sbt.internal.io 13 | 14 | import java.io.IOException 15 | import java.util.concurrent.atomic.AtomicInteger 16 | 17 | object RetrySpec extends verify.BasicTestSuite { 18 | private val noExcluded: List[Class[? <: IOException]] = List[Class[? <: IOException]]() 19 | test("retry should throw first exception after number of failures") { 20 | val i = new AtomicInteger() 21 | def throww(): Any = throw new IOException(i.incrementAndGet().toString) 22 | try { 23 | Retry(throww(), limit = 10, sleepInMillis = 10, noExcluded*) 24 | assert(false) 25 | } catch { 26 | case ioe: IOException => 27 | assert(ioe.getMessage == "1") 28 | assert(i.get() == 10) 29 | } 30 | } 31 | 32 | test("Retry.io should throw first exception after number of failures") { 33 | val i = new AtomicInteger() 34 | def throww(): Any = throw new IOException(i.incrementAndGet().toString) 35 | try { 36 | Retry.io(throww(), limit = 10, sleepInMillis = 10, noExcluded*) 37 | assert(false) 38 | } catch { 39 | case ioe: IOException => 40 | assert(ioe.getMessage == "1") 41 | assert(i.get() == 10) 42 | } 43 | } 44 | 45 | test("retry should throw recover") { 46 | for (recoveryStep <- (1 to 14)) { 47 | val i = new AtomicInteger() 48 | val value = Retry( 49 | { 50 | val thisI = i.incrementAndGet() 51 | if (thisI == recoveryStep) "recover" else throw new IOException(thisI.toString) 52 | }, 53 | limit = 15, 54 | sleepInMillis = 0, 55 | noExcluded* 56 | ) 57 | assert(value == "recover") 58 | } 59 | } 60 | 61 | test("retry should recover from non-IO exceptions") { 62 | val i = new AtomicInteger() 63 | def throww(): Any = 64 | if (i.incrementAndGet() == 5) 0 65 | else ??? 66 | Retry(throww()) 67 | () 68 | } 69 | 70 | test("Retry.io should throw non-IOException") { 71 | def throww(): Any = ??? 72 | intercept[NotImplementedError] { 73 | Retry.io(throww()) 74 | () 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /io/src/test/scala/sbt/internal/io/SourceSpec.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * sbt IO 3 | * Copyright Scala Center, Lightbend, and Mark Harrah 4 | * 5 | * Licensed under Apache License 2.0 6 | * SPDX-License-Identifier: Apache-2.0 7 | * 8 | * See the NOTICE file distributed with this work for 9 | * additional information regarding copyright ownership. 10 | */ 11 | 12 | package sbt.internal.io 13 | 14 | import java.io.File 15 | import java.nio.file.Paths 16 | 17 | import org.scalatest.flatspec.AnyFlatSpec 18 | import org.scalatest.matchers.should.Matchers 19 | import sbt.io.{ AllPassFilter, NothingFilter, SimpleFileFilter } 20 | 21 | @deprecated("Source has been replaced by Glob.", "1.3.0") 22 | class SourceSpec extends AnyFlatSpec with Matchers { 23 | it should "accept recursive paths" in { 24 | val source = new Source(new File("/foo"), AllPassFilter, NothingFilter, true) 25 | source.accept(Paths.get("/foo/bar/baz")) shouldBe true 26 | } 27 | it should "reject subdirectories without recursive flag" in { 28 | val source = new Source(new File("/foo"), AllPassFilter, NothingFilter, false) 29 | source.accept(Paths.get("/foo/bar/baz")) shouldBe false 30 | } 31 | it should "apply include filter" in { 32 | val source = new Source( 33 | new File("/foo"), 34 | new SimpleFileFilter(_.toString.endsWith(".scala")), 35 | NothingFilter, 36 | true 37 | ) 38 | source.accept(Paths.get("/foo/bar/baz.scala")) shouldBe true 39 | source.accept(Paths.get("/foo/bar/baz.java")) shouldBe false 40 | } 41 | it should "apply exclude filter" in { 42 | val source = new Source( 43 | new File("/foo"), 44 | new SimpleFileFilter(_.toString.endsWith(".scala")), 45 | new SimpleFileFilter(_ == sbt.io.syntax.file("/foo/bar/buzz.scala")), 46 | true 47 | ) 48 | source.accept(Paths.get("/foo/bar/baz.scala")) shouldBe true 49 | source.accept(Paths.get("/foo/bar/buzz.scala")) shouldBe false 50 | } 51 | it should "override equals/hashcode" in { 52 | val source = new Source(new File("foo"), AllPassFilter, NothingFilter, true) 53 | val copy = new Source(new File("foo"), AllPassFilter, NothingFilter, true) 54 | assert(source == copy && copy == source) 55 | assert(source.hashCode == copy.hashCode) 56 | val others = Seq( 57 | new Source(new File("bar"), AllPassFilter, NothingFilter, true), 58 | new Source(new File("foo"), NothingFilter, NothingFilter, true), 59 | new Source(new File("foo"), AllPassFilter, AllPassFilter, true), 60 | new Source(new File("foo"), AllPassFilter, NothingFilter, false), 61 | new Object 62 | ) 63 | others foreach { src => 64 | assert(source != src && src != source) 65 | assert(source.hashCode != src.hashCode) 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /io/src/test/scala/sbt/internal/io/WatchServiceBackedObservableSpec.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * sbt IO 3 | * Copyright Scala Center, Lightbend, and Mark Harrah 4 | * 5 | * Licensed under Apache License 2.0 6 | * SPDX-License-Identifier: Apache-2.0 7 | * 8 | * See the NOTICE file distributed with this work for 9 | * additional information regarding copyright ownership. 10 | */ 11 | 12 | package sbt 13 | package internal 14 | package io 15 | 16 | import java.nio.file.{ Files, Path, WatchKey } 17 | import java.util.concurrent.{ ConcurrentHashMap, CountDownLatch, TimeUnit } 18 | 19 | import org.scalatest.flatspec.AnyFlatSpec 20 | import sbt.internal.nio.WatchServiceBackedObservable 21 | import sbt.io._ 22 | import sbt.nio.file._ 23 | import sbt.nio.file.syntax._ 24 | 25 | import scala.collection.JavaConverters._ 26 | import scala.concurrent.duration._ 27 | 28 | class WatchServiceBackedObservableSpec extends AnyFlatSpec { 29 | "register" should "work recursively" in IO.withTemporaryDirectory { dir => 30 | val path = dir.getCanonicalFile.toPath 31 | val subdir = Files.createDirectories(path.resolve("a").resolve("b").resolve("c")).toRealPath() 32 | val watchState = 33 | new NewWatchState( 34 | ConcurrentHashMap.newKeySet[Glob].asScala, 35 | WatchService.default, 36 | new ConcurrentHashMap[Path, WatchKey].asScala 37 | ) 38 | val observable = 39 | new WatchServiceBackedObservable( 40 | watchState, 41 | 100.millis, 42 | closeService = true, 43 | (_: Any) => {} 44 | ) 45 | try { 46 | val latch = new CountDownLatch(1) 47 | val file = subdir.resolve("file") 48 | observable.addObserver(e => if (e.path == file) latch.countDown()) 49 | observable.register(path.toGlob / RecursiveGlob) 50 | Files.createFile(file) 51 | assert(latch.await(1, TimeUnit.SECONDS)) 52 | } finally observable.close() 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /io/src/test/scala/sbt/internal/nio/GlobsSpec.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * sbt IO 3 | * Copyright Scala Center, Lightbend, and Mark Harrah 4 | * 5 | * Licensed under Apache License 2.0 6 | * SPDX-License-Identifier: Apache-2.0 7 | * 8 | * See the NOTICE file distributed with this work for 9 | * additional information regarding copyright ownership. 10 | */ 11 | 12 | package sbt.internal.nio 13 | 14 | import java.io.File 15 | import java.nio.file.Files 16 | 17 | import org.scalatest.flatspec.AnyFlatSpec 18 | import sbt.io._ 19 | import sbt.nio.TestHelpers._ 20 | import sbt.nio.file.RelativeGlob.{ Matcher, NoPath } 21 | import sbt.nio.file.{ **, AnyPath, Glob } 22 | 23 | class GlobsSpec extends AnyFlatSpec { 24 | 25 | "FullFileGlob" should "apply exact name filters" in { 26 | assert(Globs(basePath, recursive = true, "foo") == Glob(basePath, ** / "foo")) 27 | assert(Globs(basePath, recursive = false, "foo") == Glob(basePath, "foo")) 28 | assert(Globs(basePath, recursive = true, "foo") / "bar" == Glob(basePath, ** / "foo" / "bar")) 29 | } 30 | it should "apply extension filters" in { 31 | assert(Globs(basePath, recursive = true, "*.scala") == Glob(basePath, ** / "*.scala")) 32 | assert(Globs(basePath, recursive = false, "*.scala") == Glob(basePath, "*.scala")) 33 | val filter: FileFilter = ("*.scala": FileFilter) || "*.java" 34 | assert(Globs(basePath, recursive = false, filter) == Glob(basePath, "*.{scala,java}")) 35 | } 36 | it should "apply prefix filters" in { 37 | assert(Globs(basePath, recursive = true, "*foo") == Glob(basePath, ** / "*foo")) 38 | assert(Globs(basePath, recursive = false, "*foo") == Glob(basePath, "*foo")) 39 | assert(Globs(basePath, recursive = true, "*foo") / "bar" == Glob(basePath, ** / "*foo" / "bar")) 40 | } 41 | it should "apply suffix filters" in { 42 | assert(Globs(basePath, recursive = true, "foo*") == Glob(basePath, ** / "foo*")) 43 | assert(Globs(basePath, recursive = false, "foo*") == Glob(basePath, "foo*")) 44 | assert(Globs(basePath, recursive = true, "foo*") / "bar" == Glob(basePath, ** / "foo*" / "bar")) 45 | } 46 | it should "apply pattern filters" in { 47 | val patternFilter: FileFilter = "foo*bar*baz" 48 | val file = new File("fooxybarzwefwebaz") 49 | assert(patternFilter.accept(file)) 50 | assert(Globs(basePath, recursive = true, patternFilter).matches(basePath.resolve(file.toPath))) 51 | } 52 | it should "apply simple filters" in { 53 | val simpleFilter: SimpleFilter = new SimpleFilter(_.startsWith("foo")) 54 | val file = new File("fooxybarzwefwebaz") 55 | assert(simpleFilter.accept(file)) 56 | assert(Globs(basePath, recursive = true, simpleFilter).matches(basePath.resolve(file.toPath))) 57 | 58 | } 59 | it should "apply not filters" in { 60 | val filter: FileFilter = "foo" 61 | val notFilter: FileFilter = -filter 62 | val glob = Globs(basePath, recursive = true, notFilter) 63 | assert(!glob.matches(basePath.resolve("foo"))) 64 | assert(glob.matches(basePath.resolve("fooo"))) 65 | 66 | val altNotFilter: FileFilter = AllPassFilter -- filter 67 | val altGlob = Globs(basePath, recursive = true, altNotFilter) 68 | assert(!altGlob.matches(basePath.resolve("foo"))) 69 | assert(altGlob.matches(basePath.resolve("fooo"))) 70 | 71 | assert(Globs(basePath, recursive = false, filter -- NothingFilter) == Glob(basePath, "foo")) 72 | } 73 | it should "apply and filters" in { 74 | val fooFilter: NameFilter = "foo*" 75 | val fooBarFilter: NameFilter = "*foobar*" 76 | val glob = Globs(basePath, recursive = false, fooFilter & fooBarFilter) 77 | val altGlob = Globs(basePath, recursive = false, fooFilter && fooBarFilter) 78 | assert(glob == Glob(basePath, Matcher.and(Matcher("foo*"), Matcher("*foobar*")))) 79 | assert(altGlob == Glob(basePath, Matcher.and(Matcher("foo*"), Matcher("*foobar*")))) 80 | assert(glob.matches(basePath.resolve("foobarbaz"))) 81 | assert(!glob.matches(basePath.resolve("barfoobarbaz"))) 82 | assert(altGlob.matches(basePath.resolve("foobarbaz"))) 83 | assert(!altGlob.matches(basePath.resolve("barfoobarbaz"))) 84 | 85 | val notNothingGlob = Globs(basePath, recursive = false, fooFilter & -NothingFilter) 86 | val altNotNothingGlob = Globs(basePath, recursive = false, fooFilter && -NothingFilter) 87 | assert(notNothingGlob == Glob(basePath, "foo*")) 88 | assert(altNotNothingGlob == Glob(basePath, "foo*")) 89 | val nothingGlob = Globs(basePath, recursive = false, fooFilter & NothingFilter) 90 | val altNothingGlob = Globs(basePath, recursive = false, fooFilter && NothingFilter) 91 | assert(nothingGlob == Glob(basePath, NoPath)) 92 | assert(altNothingGlob == Glob(basePath, NoPath)) 93 | } 94 | it should "apply or filters" in { 95 | val scalaFilter: NameFilter = "*.scala" 96 | val javaFilter: NameFilter = "*.java" 97 | val glob = Globs(basePath, recursive = false, scalaFilter | javaFilter) 98 | val alternateGlob = Globs(basePath, recursive = false, scalaFilter || javaFilter) 99 | assert(glob == Glob(basePath, "*.{scala,java}")) 100 | assert(alternateGlob == Glob(basePath, "*.{scala,java}")) 101 | val allOrGlob = Globs(basePath, recursive = false, scalaFilter | AllPassFilter) 102 | val altAllOrGlob = Globs(basePath, recursive = false, scalaFilter || AllPassFilter) 103 | assert(allOrGlob == Glob(basePath, AnyPath)) 104 | assert(altAllOrGlob == Glob(basePath, AnyPath)) 105 | val notOrGlob = Globs(basePath, recursive = false, scalaFilter | NothingFilter) 106 | val altNotOrGlob = Globs(basePath, recursive = false, scalaFilter || NothingFilter) 107 | assert(notOrGlob == Glob(basePath, "*.scala")) 108 | assert(altNotOrGlob == Glob(basePath, "*.scala")) 109 | } 110 | it should "apply arbitrary filters" in IO.withTemporaryDirectory { dir => 111 | val dirPath = dir.toPath 112 | val file = Files.createFile(dirPath.resolve("file")) 113 | val subdir = Files.createDirectories(dirPath.resolve("subdir")) 114 | val subFile = subdir.resolve("file") 115 | 116 | assert(Globs(dirPath, recursive = false, -DirectoryFilter).matches(file)) 117 | assert(!Globs(dirPath, recursive = false, -DirectoryFilter).matches(subdir)) 118 | assert(!Globs(dirPath, recursive = false, -DirectoryFilter).matches(subFile)) 119 | 120 | assert(Globs(dirPath, recursive = true, -DirectoryFilter).matches(file)) 121 | assert(!Globs(dirPath, recursive = true, -DirectoryFilter).matches(subdir)) 122 | assert(Globs(dirPath, recursive = true, -DirectoryFilter).matches(subFile)) 123 | } 124 | it should "apply anonymous filters" in IO.withTemporaryDirectory { dir => 125 | val dirPath = dir.toPath 126 | val file = Files.createFile(dirPath.resolve("file.template")) 127 | val regex = ".*\\.template".r 128 | val filter = new NameFilter { 129 | override def accept(name: String): Boolean = regex.pattern.matcher(name).matches() 130 | } 131 | assert(Globs(dirPath, recursive = false, filter).matches(file)) 132 | } 133 | it should "apply and filter with not hidden file filter" in { 134 | val filter = new ExtensionFilter("java", "scala") && -HiddenFileFilter 135 | val glob = Globs(basePath, recursive = true, filter) 136 | assert(glob.matches(basePath.resolve("foo").resolve("bar.scala"))) 137 | } 138 | it should "apply and filter with not filter" in { 139 | val filter = new ExtensionFilter("java", "scala") && -new PrefixFilter("bar") 140 | val glob = Globs(basePath, recursive = true, filter) 141 | assert(!glob.matches(basePath.resolve("foo").resolve("bar.scala"))) 142 | assert(glob.matches(basePath.resolve("foo").resolve("baz.java"))) 143 | } 144 | it should "apply or with name filters" in { 145 | val excludeFilter: FileFilter = ("Baz.scala": NameFilter) || "Bar.scala" 146 | val includeFilter: NameFilter = "*.scala" 147 | val filter = includeFilter -- excludeFilter 148 | val glob = Globs(basePath, recursive = true, filter) 149 | assert(glob.matches(basePath.resolve("foo").resolve("Foo.scala"))) 150 | assert(!glob.matches(basePath.resolve("foo").resolve("Bar.scala"))) 151 | } 152 | "hidden files" should "be included by default" in { 153 | val glob = Globs(basePath, recursive = true, "*.scala") 154 | assert(glob.matches(basePath.resolve("foo").resolve("Foo.scala"))) 155 | assert(glob.matches(basePath.resolve("foo").resolve("bar").resolve(".Bar.scala"))) 156 | } 157 | they should "be excluded by filter" in { 158 | val glob = Globs(basePath, recursive = true, ("*.scala": NameFilter) -- HiddenFileFilter) 159 | assert(glob.matches(basePath.resolve("foo").resolve("Foo.scala"))) 160 | assert( 161 | scala.util.Properties.isWin || 162 | !glob.matches(basePath.resolve("foo").resolve("bar").resolve(".Bar.scala")) 163 | ) 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /io/src/test/scala/sbt/internal/nio/MacOSXWatchServiceSpec.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * sbt IO 3 | * Copyright Scala Center, Lightbend, and Mark Harrah 4 | * 5 | * Licensed under Apache License 2.0 6 | * SPDX-License-Identifier: Apache-2.0 7 | * 8 | * See the NOTICE file distributed with this work for 9 | * additional information regarding copyright ownership. 10 | */ 11 | 12 | package sbt.internal.nio 13 | 14 | import java.nio.file.{ Files, WatchKey } 15 | import java.nio.file.StandardWatchEventKinds._ 16 | 17 | import org.scalatest.flatspec.AnyFlatSpec 18 | import sbt.internal.io.MacOSXWatchService 19 | import sbt.io.{ IO, WatchService } 20 | 21 | import scala.annotation.tailrec 22 | import scala.collection.JavaConverters._ 23 | import scala.concurrent.duration.{ Deadline => SDeadline, _ } 24 | import scala.util.Properties 25 | 26 | class MacOSXWatchServiceSpec extends AnyFlatSpec { 27 | private def pollFor(service: WatchService, duration: FiniteDuration)( 28 | cond: WatchKey => Boolean 29 | ): Boolean = { 30 | val limit = duration.fromNow 31 | @tailrec 32 | def impl(): Boolean = { 33 | val remaining = limit - SDeadline.now 34 | if (remaining > 0.seconds) { 35 | service.poll(remaining) match { 36 | case k if cond(k) => true 37 | case _ => impl() 38 | } 39 | } else false 40 | } 41 | impl() 42 | } 43 | private def test(): Unit = 44 | if (Properties.isMac) IO.withTemporaryDirectory { dirFile => 45 | val dir = dirFile.toPath.toRealPath() 46 | val foo = Files.createDirectories(dir.resolve("foo")) 47 | val foobar = Files.createDirectories(dir.resolve("foobar")) 48 | val service = new MacOSXWatchService 49 | try { 50 | service.register(foo, ENTRY_CREATE, ENTRY_DELETE, ENTRY_MODIFY) 51 | service.register(foobar, ENTRY_CREATE, ENTRY_DELETE, ENTRY_MODIFY) 52 | val fooFile = Files.createFile(foo.resolve("foo-file")) 53 | assert(pollFor(service, 5.seconds) { k => 54 | k.pollEvents.asScala.exists(_.context == fooFile.getFileName()) 55 | }) 56 | val foobarFile = Files.createFile(foo.resolve("foo-bar-file")) 57 | assert(pollFor(service, 5.seconds) { k => 58 | k.pollEvents.asScala.exists(_.context == foobarFile.getFileName()) 59 | }) 60 | } finally service.close() 61 | () 62 | } 63 | else {} 64 | "MacOSXWatchService" should "handle overlapping directories" in test() 65 | } 66 | -------------------------------------------------------------------------------- /io/src/test/scala/sbt/internal/nio/PathSyntaxSpec.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * sbt IO 3 | * Copyright Scala Center, Lightbend, and Mark Harrah 4 | * 5 | * Licensed under Apache License 2.0 6 | * SPDX-License-Identifier: Apache-2.0 7 | * 8 | * See the NOTICE file distributed with this work for 9 | * additional information regarding copyright ownership. 10 | */ 11 | 12 | package sbt.internal.nio 13 | 14 | import java.nio.file.Paths 15 | 16 | import org.scalatest.flatspec.AnyFlatSpec 17 | import sbt.nio.TestHelpers._ 18 | import sbt.nio.file.RelativeGlob 19 | import sbt.nio.file.syntax._ 20 | import sbt.nio.file._ 21 | import sbt.nio.file.Glob.GlobOps 22 | 23 | class PathSyntaxSpec extends AnyFlatSpec { 24 | "toGlob" should "work with absolute paths" in { 25 | assert(basePath.toGlob.matches(basePath)) 26 | assert(!basePath.toGlob.matches(basePath.getParent)) 27 | assert(!basePath.toGlob.matches(basePath / "foo")) 28 | } 29 | it should "work with relative paths" in { 30 | val base = Paths.get("foo") / "bar" 31 | val baseGlob = base.toGlob 32 | assert(baseGlob.matches(base)) 33 | baseGlob match { 34 | case r: RelativeGlob => 35 | val absoluteGlob = basePath.toGlob / r 36 | val absolutePath = basePath.resolve(base) 37 | assert(absoluteGlob.base == basePath.resolve(base)) 38 | assert(absoluteGlob.matches(basePath.resolve(base))) 39 | val txtGlob = absoluteGlob / ** / "*.txt" 40 | assert(txtGlob.matches(absolutePath / "foo" / "bar" / "baz.txt")) 41 | assert(!txtGlob.matches(absolutePath)) 42 | assert(!txtGlob.matches(absolutePath.getParent / "foo.txt")) 43 | case _ => throw new IllegalStateException("Relative path was not converted to relative glob") 44 | } 45 | } 46 | it should "work with empty paths" in { 47 | val empty = Paths.get("").toGlob match { 48 | case r: RelativeGlob => r 49 | case _ => throw new IllegalStateException("Relative path was not converted to relative glob") 50 | } 51 | val glob = basePath.toGlob / empty / ** / empty / empty / "*.txt" 52 | assert(glob.matches(basePath / "foo.txt")) 53 | assert(glob.matches(basePath / "foo" / "bar" / "baz.txt")) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /io/src/test/scala/sbt/io/CombinedFilterSpec.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * sbt IO 3 | * Copyright Scala Center, Lightbend, and Mark Harrah 4 | * 5 | * Licensed under Apache License 2.0 6 | * SPDX-License-Identifier: Apache-2.0 7 | * 8 | * See the NOTICE file distributed with this work for 9 | * additional information regarding copyright ownership. 10 | */ 11 | 12 | package sbt.io 13 | import java.io.File 14 | 15 | import org.scalatest.flatspec.AnyFlatSpec 16 | import org.scalatest.matchers.should.Matchers 17 | 18 | class CombinedFilterSpec extends AnyFlatSpec with Matchers { 19 | def endsWithTxt: String => Boolean = new Function[String, Boolean] { 20 | override def apply(string: String): Boolean = string.endsWith("txt") 21 | } 22 | "FileFilter" should "combine filters with &&" in IO.withTemporaryDirectory { dir => 23 | val firstFilter = new ExactFilter(dir.getName) 24 | assert(firstFilter.accept(dir)) 25 | val andFilter = firstFilter && DirectoryFilter 26 | assert(andFilter.accept(dir)) 27 | assert(andFilter.toString == s"$firstFilter && DirectoryFilter") 28 | } 29 | it should "combine filters with &" in IO.withTemporaryDirectory { dir => 30 | val file = new File(dir, "foo.txt") 31 | val firstFilter = new ExactFilter("foo.txt") 32 | assert(firstFilter.accept(file)) 33 | val endsWith = endsWithTxt 34 | val andFilter = firstFilter && new SimpleFilter(endsWith) 35 | assert(andFilter.accept(file)) 36 | // This is subtle. I'm checking that equality works when the same function instance is used 37 | // in the SimpleFilter, but a new SimpleFilter is created itself. In the second test case, 38 | // equality doesn't work because endsWithText creates a new instance of String => Boolean 39 | // which can't be equal to the endsWithText local variable. 40 | assert(andFilter == (firstFilter && new SimpleFilter(endsWith))) 41 | assert(andFilter != (firstFilter && new SimpleFilter(endsWithTxt))) 42 | } 43 | it should "combine filters with ||" in IO.withTemporaryDirectory { dir => 44 | val firstFilter = new ExactFilter("foo.txt") 45 | assert(!firstFilter.accept(dir)) 46 | val orFilter = firstFilter || DirectoryFilter 47 | assert(orFilter.accept(dir)) 48 | assert(orFilter.toString == s"ExactFilter(foo.txt) || DirectoryFilter") 49 | } 50 | it should "combine filters with |" in IO.withTemporaryDirectory { dir => 51 | val file = new File("bar.txt") 52 | val firstFilter = new ExactFilter("foo.txt") 53 | assert(!firstFilter.accept(file)) 54 | val orFilter = firstFilter | new ExactFilter("bar.txt") 55 | assert(orFilter.accept(file)) 56 | } 57 | it should "combine filters with --" in IO.withTemporaryDirectory { dir => 58 | val firstFilter = new ExactFilter(dir.getName) 59 | assert(firstFilter.accept(dir.getName)) 60 | val andNotFilter = firstFilter -- DirectoryFilter 61 | assert(!andNotFilter.accept(dir)) 62 | assert(andNotFilter.toString == s"ExactFilter(${dir.getName}) && !DirectoryFilter") 63 | } 64 | it should "combine filters with -" in { 65 | val file = new File("foo.scala") 66 | val firstFilter = new ExactFilter("foo.scala") 67 | assert(firstFilter.accept(file)) 68 | val andNotFilter = firstFilter - new SimpleFilter(_.endsWith("scala")) 69 | assert(!andNotFilter.accept(file)) 70 | } 71 | it should "negate filters" in { 72 | val file = new File("foo.scala") 73 | val firstFilter = new ExactFilter("foo.scala") 74 | assert(firstFilter.accept(file)) 75 | val notFilter = -firstFilter 76 | assert(!notFilter.accept(file)) 77 | assert(notFilter.toString == s"!ExactFilter(foo.scala)") 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /io/src/test/scala/sbt/io/CopyDirectorySpec.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * sbt IO 3 | * Copyright Scala Center, Lightbend, and Mark Harrah 4 | * 5 | * Licensed under Apache License 2.0 6 | * SPDX-License-Identifier: Apache-2.0 7 | * 8 | * See the NOTICE file distributed with this work for 9 | * additional information regarding copyright ownership. 10 | */ 11 | 12 | package sbt.io 13 | 14 | import java.nio.file._ 15 | import org.scalatest.flatspec.AnyFlatSpec 16 | import sbt.io.syntax._ 17 | 18 | class CopyDirectorySpec extends AnyFlatSpec { 19 | it should "copy symlinks" in IO.withTemporaryDirectory { dir => 20 | // Given: 21 | // src/ 22 | // actual/ 23 | // a.txt 24 | // lib/ 25 | // a.txt -> ../actual/a.txt 26 | 27 | val srcFile1 = dir / "src" / "actual" / "a.txt" 28 | val srcFile2 = dir / "src" / "lib" / "a.txt" 29 | 30 | IO.write(srcFile1, "this is the file contents") 31 | 32 | IO.createDirectory(srcFile2.getParentFile) 33 | Files.createSymbolicLink(srcFile2.toPath, Paths.get("../actual/a.txt")) 34 | 35 | // When: the "src" directory is copied to "dst" 36 | IO.copyDirectory(dir / "src", dir / "dst") 37 | 38 | // Then: dst/lib/a.txt should have been created and have the correct contents 39 | assert(IO.read(dir / "dst" / "lib" / "a.txt") == "this is the file contents") 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /io/src/test/scala/sbt/io/CopySpec.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * sbt IO 3 | * Copyright Scala Center, Lightbend, and Mark Harrah 4 | * 5 | * Licensed under Apache License 2.0 6 | * SPDX-License-Identifier: Apache-2.0 7 | * 8 | * See the NOTICE file distributed with this work for 9 | * additional information regarding copyright ownership. 10 | */ 11 | 12 | package sbt.io 13 | 14 | import java.io.File 15 | import java.util.Arrays 16 | import scala.annotation.tailrec 17 | import org.scalacheck._, Arbitrary.arbLong, Prop._ 18 | 19 | object CopySpec extends Properties("Copy") { 20 | // set to 0.25 GB by default for success on most systems without running out of space. 21 | // when modifying IO.copyFile, verify against 1 GB or higher, preferably > 4 GB 22 | final val MaxFileSizeBits = 28 23 | final val BufferSize = 1 * 1024 * 1024 24 | 25 | val randomSize = Gen.choose(0, MaxFileSizeBits).map(1L << _) 26 | val pow2Size = (0 until MaxFileSizeBits).toList.map(1L << _) 27 | val derivedSize = pow2Size.map(_ - 1) ::: pow2Size.map(_ + 1) ::: pow2Size 28 | 29 | val fileSizeGen: Gen[Long] = 30 | Gen.frequency( 31 | 80 -> Gen.oneOf(derivedSize), 32 | 8 -> randomSize, 33 | 1 -> Gen.const(0) 34 | ) 35 | 36 | property("same contents") = forAll(fileSizeGen, arbLong.arbitrary)((size: Long, seed: Long) => 37 | IO.withTemporaryDirectory { dir => 38 | val f1 = new File(dir, "source") 39 | val f2 = new File(dir, "dest") 40 | generate(seed = seed, size = size, file = f1) 41 | IO.copyFile(f1, f2) 42 | checkContentsSame(f1, f2) 43 | true 44 | } 45 | ) 46 | 47 | def generate(seed: Long, size: Long, file: File) = { 48 | val rnd = new java.util.Random(seed) 49 | 50 | val buffer = new Array[Byte](BufferSize) 51 | @tailrec def loop(offset: Long): Unit = { 52 | val len = math.min(size - offset, BufferSize.toLong) 53 | if (len > 0) { 54 | rnd.nextBytes(buffer) 55 | IO.append(file, buffer) 56 | loop(offset + len) 57 | } 58 | } 59 | if (size == 0L) IO.touch(file) else loop(0) 60 | } 61 | 62 | def checkContentsSame(f1: File, f2: File) = { 63 | val len = f1.length 64 | assert( 65 | len == f2.length, 66 | "File lengths differ: " + (len, f2.length).toString + " for " + (f1, f2).toString 67 | ) 68 | Using.fileInputStream(f1) { in1 => 69 | Using.fileInputStream(f2) { in2 => 70 | val buffer1 = new Array[Byte](BufferSize) 71 | val buffer2 = new Array[Byte](BufferSize) 72 | @tailrec def loop(offset: Long): Unit = if (offset < len) { 73 | val read1 = in1.read(buffer1) 74 | val read2 = in2.read(buffer2) 75 | assert( 76 | read1 == read2, 77 | "Read " + (read1, read2).toString + " bytes from " + (f1, f2).toString 78 | ) 79 | assert(Arrays.equals(buffer1, buffer2), "Contents differed.") 80 | loop(offset + read1) 81 | } 82 | loop(0) 83 | } 84 | } 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /io/src/test/scala/sbt/io/FileSpec.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * sbt IO 3 | * Copyright Scala Center, Lightbend, and Mark Harrah 4 | * 5 | * Licensed under Apache License 2.0 6 | * SPDX-License-Identifier: Apache-2.0 7 | * 8 | * See the NOTICE file distributed with this work for 9 | * additional information regarding copyright ownership. 10 | */ 11 | 12 | package sbt.io 13 | 14 | import org.scalatest.flatspec.AnyFlatSpec 15 | import org.scalatest.matchers.should.Matchers 16 | import sbt.io.syntax._ 17 | import java.nio.file.attribute.PosixFilePermission 18 | 19 | class FileSpec extends AnyFlatSpec with Matchers { 20 | "files" should "set/unset permissions" in { 21 | IO.withTemporaryDirectory { dir => 22 | val t1 = dir / "foo.txt" 23 | IO.write(t1, "foo") 24 | if (IO.isPosix) { 25 | t1.permissions(PosixFilePermission.OWNER_EXECUTE) shouldBe false 26 | 27 | t1.addPermission(PosixFilePermission.OWNER_EXECUTE) 28 | t1.addPermission(PosixFilePermission.GROUP_WRITE) 29 | t1.testPermission(PosixFilePermission.OWNER_EXECUTE) shouldBe true 30 | t1.permissionsAsString should fullyMatch regex "..x.w...." 31 | 32 | t1.removePermission(PosixFilePermission.OWNER_EXECUTE) 33 | t1.isOwnerExecutable shouldBe false 34 | t1.permissionsAsString should fullyMatch regex "..-.w...." 35 | } else () 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /io/src/test/scala/sbt/io/FileUtilitiesSpecification.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * sbt IO 3 | * Copyright Scala Center, Lightbend, and Mark Harrah 4 | * 5 | * Licensed under Apache License 2.0 6 | * SPDX-License-Identifier: Apache-2.0 7 | * 8 | * See the NOTICE file distributed with this work for 9 | * additional information regarding copyright ownership. 10 | */ 11 | 12 | package sbt.io 13 | 14 | import java.io.File 15 | import org.scalacheck._, Arbitrary.arbitrary, Prop._ 16 | 17 | object WriteContentSpecification extends Properties("Write content") { 18 | sys.props.put("jna.nosys", "true") 19 | 20 | property("Round trip string") = forAll(writeAndCheckString _) 21 | property("Round trip bytes") = forAll(writeAndCheckBytes _) 22 | property("Write string overwrites") = forAll(overwriteAndCheckStrings _) 23 | property("Write bytes overwrites") = forAll(overwriteAndCheckBytes _) 24 | property("Append string appends") = forAll(appendAndCheckStrings _) 25 | property("Append bytes appends") = forAll(appendAndCheckBytes _) 26 | property("Unzip doesn't stack overflow") = largeUnzip() 27 | property("Unzip errors given parent traversal") = testZipSlip() 28 | 29 | implicit lazy val validChar: Arbitrary[Char] = Arbitrary( 30 | for (i <- Gen.choose(0, 0xd7ff)) yield i.toChar 31 | ) 32 | 33 | implicit lazy val validString: Arbitrary[String] = Arbitrary( 34 | arbitrary[List[Char]] map (_.mkString) 35 | ) 36 | 37 | private def largeUnzip() = { 38 | testUnzip[Product] 39 | testUnzip[scala.tools.nsc.Global] 40 | true 41 | } 42 | 43 | private def testZipSlip() = { 44 | val badFile0 = new File("io/src/test/resources/zip-slip.zip") 45 | val badFile1 = new File("src/test/resources/zip-slip.zip") 46 | val badFile = 47 | if (badFile0.exists()) badFile0 48 | else badFile1 49 | try { 50 | unzipFile(badFile) 51 | false 52 | } catch { 53 | case e: RuntimeException => e.getMessage.contains("outside of the target directory") 54 | } 55 | } 56 | 57 | private def testUnzip[T](implicit mf: Manifest[T]) = 58 | unzipFile(IO.classLocationFileOption(mf.runtimeClass).getOrElse(sys.error(s"$mf"))) 59 | 60 | private def unzipFile(jar: File) = IO.withTemporaryDirectory(tmp => IO.unzip(jar, tmp)) 61 | 62 | // make the test independent of underlying platform and allow any unicode character in Strings to be encoded 63 | val charset = IO.utf8 64 | 65 | private def writeAndCheckString(s: String) = 66 | withTemporaryFile { file => 67 | IO.write(file, s, charset) 68 | IO.read(file, charset) == s 69 | } 70 | 71 | private def writeAndCheckBytes(b: Array[Byte]) = 72 | withTemporaryFile { file => 73 | IO.write(file, b) 74 | IO.readBytes(file) sameElements b 75 | } 76 | 77 | private def overwriteAndCheckStrings(a: String, b: String) = 78 | withTemporaryFile { file => 79 | IO.write(file, a, charset) 80 | IO.write(file, b, charset) 81 | IO.read(file, charset) == b 82 | } 83 | 84 | private def overwriteAndCheckBytes(a: Array[Byte], b: Array[Byte]) = 85 | withTemporaryFile { file => 86 | IO.write(file, a) 87 | IO.write(file, b) 88 | IO.readBytes(file) sameElements b 89 | } 90 | 91 | private def appendAndCheckStrings(a: String, b: String) = 92 | withTemporaryFile { file => 93 | IO.append(file, a, charset) 94 | IO.append(file, b, charset) 95 | IO.read(file, charset) == (a + b) 96 | } 97 | 98 | private def appendAndCheckBytes(a: Array[Byte], b: Array[Byte]) = 99 | withTemporaryFile { file => 100 | IO.append(file, a) 101 | IO.append(file, b) 102 | IO.readBytes(file) sameElements (a ++ b) 103 | } 104 | 105 | private def withTemporaryFile[T](f: File => T): T = 106 | IO.withTemporaryDirectory(dir => f(new File(dir, "out"))) 107 | } 108 | -------------------------------------------------------------------------------- /io/src/test/scala/sbt/io/GlobFilterSpec.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * sbt IO 3 | * Copyright Scala Center, Lightbend, and Mark Harrah 4 | * 5 | * Licensed under Apache License 2.0 6 | * SPDX-License-Identifier: Apache-2.0 7 | * 8 | * See the NOTICE file distributed with this work for 9 | * additional information regarding copyright ownership. 10 | */ 11 | 12 | package sbt.io 13 | import java.io.File 14 | 15 | import org.scalatest.flatspec.AnyFlatSpec 16 | import org.scalatest.matchers.should.Matchers 17 | 18 | class GlobFilterSpec extends AnyFlatSpec with Matchers { 19 | "GlobFilter" should "work with *" in { 20 | assert(GlobFilter("*") == AllPassFilter) 21 | } 22 | it should "work with no *" in { 23 | assert(GlobFilter("foo.txt") == new ExactFilter("foo.txt")) 24 | assert(GlobFilter("foo.txt").accept("foo.txt")) 25 | } 26 | it should "work with simple extensions" in { 27 | assert(GlobFilter("*.txt") == new ExtensionFilter("txt")) 28 | assert(GlobFilter("*.txt").accept("foo.txt")) 29 | } 30 | it should "combine extensions" in { 31 | assert((GlobFilter("*.scala") && GlobFilter("*.java")) == new ExtensionFilter()) 32 | assert((GlobFilter("*.scala") & GlobFilter("*.java")) == new ExtensionFilter()) 33 | assert((GlobFilter("*.scala") || GlobFilter("*.java")) == new ExtensionFilter("scala", "java")) 34 | assert((GlobFilter("*.scala") | GlobFilter("*.java")) == new ExtensionFilter("scala", "java")) 35 | val scalaFilter = new ExtensionFilter("scala") 36 | assert((GlobFilter("*.scala") || GlobFilter("*.java") -- GlobFilter("*.java")) == scalaFilter) 37 | assert((GlobFilter("*.scala") || GlobFilter("*.java") - GlobFilter("*.java")) == scalaFilter) 38 | assert((GlobFilter("*.scala") -- ExistsFileFilter).accept(new File("foo.scala"))) 39 | assert(!(GlobFilter("*.scala") && ExistsFileFilter).accept(new File("foo.scala"))) 40 | assert((GlobFilter("*.scala") || DirectoryFilter).accept(new File("."))) 41 | } 42 | it should "work with patterns" in { 43 | val filter = GlobFilter("foo*.txt") 44 | assert(filter.accept(new File("foobar.txt"))) 45 | assert(!filter.accept(new File("bar.txt"))) 46 | } 47 | it should "work with trailing *" in { 48 | val filter = GlobFilter("foo*") 49 | assert(filter.isInstanceOf[PrefixFilter]) 50 | assert(filter.accept(new File("foobar.txt"))) 51 | assert(!filter.accept(new File("fobar.txt"))) 52 | } 53 | it should "work with leading *" in { 54 | val filter = GlobFilter("*foo.txt") 55 | assert(filter.isInstanceOf[SuffixFilter]) 56 | assert(filter.accept(new File("foo.txt"))) 57 | assert(filter.accept(new File("afoo.txt"))) 58 | assert(!filter.accept(new File("afoo.txta"))) 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /io/src/test/scala/sbt/io/IOSpecification.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * sbt IO 3 | * Copyright Scala Center, Lightbend, and Mark Harrah 4 | * 5 | * Licensed under Apache License 2.0 6 | * SPDX-License-Identifier: Apache-2.0 7 | * 8 | * See the NOTICE file distributed with this work for 9 | * additional information regarding copyright ownership. 10 | */ 11 | 12 | package sbt.io 13 | 14 | import scala.util.Try 15 | import org.scalacheck._, Prop._ 16 | import java.nio.file.Files 17 | 18 | object IOSpecification extends Properties("IO") { 19 | property("IO.classLocationPath able to determine containing directories") = forAll(classes) { 20 | (c: Class[?]) => 21 | Try(IO.classLocationPath(c)).toOption.exists { 22 | case jar if jar.getFileName.toString.endsWith(".jar") => 23 | Files.isRegularFile(jar) 24 | case jrt if jrt.getFileSystem.provider.getScheme == "jrt" => 25 | jrt.toString.contains("/java.base") 26 | case dir => 27 | Files.isDirectory(dir) 28 | } 29 | } 30 | 31 | implicit def classes: Gen[Class[?]] = 32 | Gen.oneOf( 33 | this.getClass, 34 | classOf[java.lang.Integer], 35 | classOf[java.util.AbstractMap.SimpleEntry[String, String]], 36 | classOf[String], 37 | classOf[Thread], 38 | classOf[Properties] 39 | ) 40 | } 41 | -------------------------------------------------------------------------------- /io/src/test/scala/sbt/io/IOSyntaxSpec.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * sbt IO 3 | * Copyright Scala Center, Lightbend, and Mark Harrah 4 | * 5 | * Licensed under Apache License 2.0 6 | * SPDX-License-Identifier: Apache-2.0 7 | * 8 | * See the NOTICE file distributed with this work for 9 | * additional information regarding copyright ownership. 10 | */ 11 | 12 | package sbt.io 13 | 14 | import java.io.{ File => JFile } 15 | import org.scalatest.flatspec.AnyFlatSpec 16 | import org.scalatest.matchers.should.Matchers 17 | import sbt.io.syntax._ 18 | 19 | class IOSyntaxSpec extends AnyFlatSpec with Matchers { 20 | "file(...)" should "create File" in { 21 | file(".") shouldBe (new JFile(".")) 22 | } 23 | "file(...) / \"a\"" should "create File" in { 24 | (file("project") / "build.properties") shouldBe 25 | new JFile(new JFile("project"), "build.properties") 26 | } 27 | "file(...) glob \"*.properties\"" should "create PathFinder" in { 28 | IO.withTemporaryDirectory { dir => 29 | IO.write(new JFile(dir, "foo.txt"), "foo") 30 | IO.write(new JFile(dir, "bar.json"), "{}") 31 | (dir glob "*.txt").get() shouldBe Seq(new JFile(dir, "foo.txt")) 32 | } 33 | } 34 | "get" should "work with PathLister and PathFinder" in IO.withTemporaryDirectory { dir => 35 | assert((dir: PathLister).get() == (dir: PathFinder).get()) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /io/src/test/scala/sbt/io/LastModifiedSpec.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * sbt IO 3 | * Copyright Scala Center, Lightbend, and Mark Harrah 4 | * 5 | * Licensed under Apache License 2.0 6 | * SPDX-License-Identifier: Apache-2.0 7 | * 8 | * See the NOTICE file distributed with this work for 9 | * additional information regarding copyright ownership. 10 | */ 11 | 12 | package sbt.io 13 | 14 | import java.nio.file.{ Files, Paths => JPaths } 15 | 16 | import org.scalatest.flatspec.AnyFlatSpec 17 | import sbt.nio.file.syntax._ 18 | 19 | class LastModifiedSpec extends AnyFlatSpec { 20 | "IO.getModifiedTimeOrZero" should "work with long path names" in IO.withTemporaryDirectory { 21 | dir => 22 | val fileName = "a" * 32 23 | val nested = 24 | (1 to 8).foldLeft(dir.toPath) { case (d, _) => 25 | Files.createDirectories(d.resolve(fileName)) 26 | } 27 | val file = Files.createFile(nested.resolve(fileName)).toFile 28 | // in case target platform only has second precision round to nearest second 29 | val lm = (System.currentTimeMillis / 1000) * 1000 30 | IO.setModifiedTimeOrFalse(file, lm) 31 | assert(IO.getModifiedTimeOrZero(file) == lm) 32 | } 33 | it should "handle empty paths" in { 34 | assert(IO.getModifiedTimeOrZero(JPaths.get("").toFile) > 0) 35 | val newLM = ((System.currentTimeMillis + 10000) / 1000) * 1000 36 | IO.setModifiedTimeOrFalse(JPaths.get("").toFile, newLM) 37 | assert(IO.getModifiedTimeOrZero(JPaths.get("").toFile) == newLM) 38 | } 39 | it should "handle relative paths" in IO.withTemporaryDirectory { dir => 40 | val dirPath = dir.toPath 41 | val subDir1 = Files.createDirectories(dirPath / "subdir-1") 42 | val subDir2 = Files.createDirectories(dirPath / "subdir-2") 43 | val subFile = Files.createFile(subDir2 / "file") 44 | val lm = IO.getModifiedTimeOrZero(subFile.toFile) 45 | val relative = subDir1 / ".." / subDir2.getFileName.toString / subFile.getFileName.toString 46 | assert(IO.getModifiedTimeOrZero(relative.toFile) == lm) 47 | val newLM = ((System.currentTimeMillis + 10000) / 1000) * 1000 48 | IO.setModifiedTimeOrFalse(relative.toFile, newLM) 49 | assert(IO.getModifiedTimeOrZero(relative.toFile) == newLM) 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /io/src/test/scala/sbt/io/NameFilterSpec.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * sbt IO 3 | * Copyright Scala Center, Lightbend, and Mark Harrah 4 | * 5 | * Licensed under Apache License 2.0 6 | * SPDX-License-Identifier: Apache-2.0 7 | * 8 | * See the NOTICE file distributed with this work for 9 | * additional information regarding copyright ownership. 10 | */ 11 | 12 | package sbt.io 13 | 14 | import java.util.regex.Pattern 15 | 16 | import org.scalatest.flatspec.AnyFlatSpec 17 | import org.scalatest.matchers.should.Matchers 18 | 19 | import scala.annotation.tailrec 20 | 21 | class NameFilterSpec extends AnyFlatSpec with Matchers { 22 | "NameFilter" should "have readable toString() method" in { 23 | AllPassFilter.toString shouldBe "AllPassFilter" 24 | DirectoryFilter.toString shouldBe "DirectoryFilter" 25 | ExistsFileFilter.toString shouldBe "ExistsFileFilter" 26 | HiddenFileFilter.toString shouldBe "HiddenFileFilter" 27 | NothingFilter.toString shouldBe "NothingFilter" 28 | new ExactFilter("foo.txt").toString shouldBe s"ExactFilter(foo.txt)" 29 | new PrefixFilter("foo").toString shouldBe s"PrefixFilter(foo)" 30 | new SuffixFilter("foo").toString shouldBe s"SuffixFilter(foo)" 31 | new PatternFilter(Pattern.compile(".*\\.scala")).toString shouldBe s"PatternFilter(.*\\.scala)" 32 | } 33 | it should "correctly override equals/hashCode" in { 34 | val (foo, bar) = ("foo.txt", "bar.txt") 35 | assert(new ExactFilter(foo) == new ExactFilter(foo)) 36 | assert(new ExactFilter(foo).hashCode == new ExactFilter(foo).hashCode) 37 | assert(new ExactFilter(foo) != new ExactFilter(bar)) 38 | assert(new ExactFilter(foo).hashCode != new ExactFilter(bar).hashCode) 39 | 40 | val (fooPrefix, barPrefix) = ("foo", "bar") 41 | assert(new PrefixFilter(fooPrefix) == new PrefixFilter(fooPrefix)) 42 | assert(new PrefixFilter(barPrefix) == new PrefixFilter(barPrefix)) 43 | assert(new PrefixFilter(fooPrefix) != new PrefixFilter(barPrefix)) 44 | assert(new PrefixFilter(fooPrefix).hashCode == fooPrefix.hashCode) 45 | assert(new PrefixFilter(barPrefix).hashCode == barPrefix.hashCode) 46 | assert(new PrefixFilter(fooPrefix).hashCode != new PrefixFilter(barPrefix).hashCode) 47 | 48 | val (fooSuffix, barSuffix) = ("foo", "bar") 49 | assert(new SuffixFilter(fooSuffix) == new SuffixFilter(fooSuffix)) 50 | assert(new SuffixFilter(barSuffix) == new SuffixFilter(barSuffix)) 51 | assert(new SuffixFilter(fooSuffix) != new SuffixFilter(barSuffix)) 52 | assert(new SuffixFilter(fooSuffix).hashCode == fooSuffix.hashCode) 53 | assert(new SuffixFilter(barSuffix).hashCode == barSuffix.hashCode) 54 | assert(new SuffixFilter(fooSuffix).hashCode != new SuffixFilter(barSuffix).hashCode) 55 | 56 | val (java, scala) = (Pattern.compile(".*\\.java"), Pattern.compile(".*\\.scala")) 57 | assert(new PatternFilter(java) == new PatternFilter(java)) 58 | assert(new PatternFilter(java).hashCode == new PatternFilter(java).hashCode) 59 | assert(new PatternFilter(java) != new PatternFilter(scala)) 60 | assert(new PatternFilter(java).hashCode != new PatternFilter(scala).hashCode) 61 | } 62 | it should "correctly identify unequal filters" in { 63 | val filters = Seq( 64 | AllPassFilter, 65 | DirectoryFilter, 66 | ExistsFileFilter, 67 | HiddenFileFilter, 68 | NothingFilter, 69 | new ExactFilter("foo.txt"), 70 | new PrefixFilter("foo"), 71 | new SuffixFilter("bar"), 72 | new PatternFilter(Pattern.compile(".*\\.scala")) 73 | ) 74 | @tailrec 75 | def check(head: FileFilter, tail: Seq[FileFilter]): Unit = { 76 | tail match { 77 | case t if t.nonEmpty => 78 | t.foreach { filter => 79 | assert(head != filter && filter != head) 80 | assert(head.hashCode != filter.hashCode()) 81 | } 82 | check(t.head, t.tail) 83 | case _ => () 84 | } 85 | } 86 | check(filters.head, filters.tail) 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /io/src/test/scala/sbt/io/NameFilterSpecification.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * sbt IO 3 | * Copyright Scala Center, Lightbend, and Mark Harrah 4 | * 5 | * Licensed under Apache License 2.0 6 | * SPDX-License-Identifier: Apache-2.0 7 | * 8 | * See the NOTICE file distributed with this work for 9 | * additional information regarding copyright ownership. 10 | */ 11 | 12 | package sbt.io 13 | 14 | import org.scalacheck._, Prop._ 15 | 16 | object NameFilterSpecification extends Properties("NameFilter") { 17 | property("All pass accepts everything") = forAll((s: String) => AllPassFilter.accept(s)) 18 | 19 | property("Exact filter matches provided string") = 20 | forAll((s1: String, s2: String) => (new ExactFilter(s1)).accept(s2) == (s1 == s2)) 21 | 22 | property("Exact filter matches valid string") = 23 | forAll((s: String) => (new ExactFilter(s)).accept(s)) 24 | 25 | property("Glob filter matches provided string if no *s") = forAll { (s1: String, s2: String) => 26 | val stripped = stripAsterisksAndControl(s1) 27 | (GlobFilter(stripped).accept(s2) == (stripped == s2)) 28 | } 29 | 30 | property("Glob filter matches valid string if no *s") = forAll { (s: String) => 31 | val stripped = stripAsterisksAndControl(s) 32 | GlobFilter(stripped).accept(stripped) 33 | } 34 | 35 | property("Glob filter matches valid") = forAll { (list: List[String]) => 36 | val stripped = list.map(stripAsterisksAndControl) 37 | GlobFilter(stripped.mkString("*")).accept(stripped.mkString) 38 | } 39 | 40 | /** 41 | * Raw control characters are stripped because they are not allowed in expressions. 42 | * Asterisks are stripped because they are added under the control of the tests. 43 | */ 44 | private def stripAsterisksAndControl(s: String) = (s filter validChar).toString 45 | 46 | private[this] def validChar(c: Char) = ( 47 | !java.lang.Character.isISOControl(c) 48 | && c != '*' 49 | && !Character.isHighSurrogate(c) 50 | && !Character.isLowSurrogate(c) 51 | ) 52 | } 53 | -------------------------------------------------------------------------------- /io/src/test/scala/sbt/io/PathFinderCombinatorSpec.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * sbt IO 3 | * Copyright Scala Center, Lightbend, and Mark Harrah 4 | * 5 | * Licensed under Apache License 2.0 6 | * SPDX-License-Identifier: Apache-2.0 7 | * 8 | * See the NOTICE file distributed with this work for 9 | * additional information regarding copyright ownership. 10 | */ 11 | 12 | package sbt.io 13 | 14 | import java.nio.file.Files 15 | 16 | import org.scalatest.flatspec.AnyFlatSpec 17 | import sbt.io.syntax._ 18 | 19 | class PathFinderCombinatorSpec extends AnyFlatSpec { 20 | "PathFinderCombinator" should "provide extension methods for File" in IO.withTemporaryDirectory { 21 | dir => 22 | val file = Files.createFile(dir.toPath.resolve("file")).toFile 23 | assert((dir +++ file).get() == Seq(dir, file)) 24 | assert(((dir: PathFinder.Combinator) +++ file).get() == Seq(dir, file)) 25 | assert(((dir: PathFinder) +++ file).get() == Seq(dir, file)) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /io/src/test/scala/sbt/io/PathFinderSpec.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * sbt IO 3 | * Copyright Scala Center, Lightbend, and Mark Harrah 4 | * 5 | * Licensed under Apache License 2.0 6 | * SPDX-License-Identifier: Apache-2.0 7 | * 8 | * See the NOTICE file distributed with this work for 9 | * additional information regarding copyright ownership. 10 | */ 11 | 12 | package sbt.io 13 | 14 | import java.io.File 15 | import java.nio.file.Files 16 | 17 | import org.scalatest.flatspec.AnyFlatSpec 18 | import org.scalatest.matchers.should.Matchers 19 | 20 | import scala.collection.mutable 21 | 22 | object PathFinderSpec { 23 | implicit class FileOps(val file: File) extends AnyVal { 24 | def all(implicit handler: (File, FileFilter, mutable.Set[File]) => Unit): Seq[File] = 25 | PathFinder(file).globRecursive(AllPassFilter, handler).get() 26 | } 27 | } 28 | trait PathFinderSpec extends AnyFlatSpec with Matchers { 29 | import PathFinderSpec._ 30 | implicit def handler: (File, FileFilter, mutable.Set[File]) => Unit 31 | "PathFinder" should "find the files in a directory" in IO.withTemporaryDirectory { dir => 32 | val foo = Files.createTempFile(dir.toPath, "foo", "").toFile 33 | val bar = Files.createTempFile(dir.toPath, "bar", "").toFile 34 | dir.all.toSet shouldBe Set(dir, foo, bar) 35 | } 36 | it should "find children of subdirectories" in IO.withTemporaryDirectory { dir => 37 | val subdir = Files.createTempDirectory(dir.toPath, "subdir") 38 | val foo = Files.createTempFile(subdir, "foo", "").toFile 39 | dir.all.toSet shouldBe Set(dir, subdir.toFile, foo) 40 | } 41 | it should "apply filter" in IO.withTemporaryDirectory { dir => 42 | val foo = Files.createTempFile(dir.toPath, "foo", "").toFile 43 | Files.createTempFile(dir.toPath, "bar", "").toFile 44 | val include = new SimpleFilter(_.startsWith("foo")) 45 | PathFinder(dir).descendantsExcept(include, NothingFilter).get() shouldBe Seq(foo) 46 | } 47 | it should "apply exclude filter" in IO.withTemporaryDirectory { dir => 48 | val excludeDir = Files.createDirectories(dir.toPath.resolve("sbt-0.13")) 49 | val includeDir = Files.createDirectories(dir.toPath.resolve("sbt-1.0")) 50 | val Seq(excludeFile, includeFile) = Seq(excludeDir, includeDir).map { d => 51 | val src = Files.createDirectories(d.resolve("src").resolve("main").resolve("scala")) 52 | Files.createFile(src.resolve("foo.scala")).toFile 53 | } 54 | val files = PathFinder(dir).descendantsExcept("*.scala", s"sbt-0.13").get() 55 | assert(files == Seq(includeFile)) 56 | assert((PathFinder(dir) ** "*.scala").get().toSet == Set(excludeFile, includeFile)) 57 | } 58 | it should "apply nothing filter" in IO.withTemporaryDirectory { dir => 59 | val dirPath = dir.toPath 60 | val subdir = Files.createDirectories(dirPath.resolve("subdir")).toFile 61 | val file = Files.createFile(dirPath.resolve("file")).toFile 62 | PathFinder(dir).descendantsExcept("*", "*sub*").get().toSet shouldBe Set(dir, file) 63 | PathFinder(dir).descendantsExcept("*", NothingFilter).get().toSet shouldBe Set( 64 | dir, 65 | file, 66 | subdir 67 | ) 68 | } 69 | it should "work for complex extension filters" in IO.withTemporaryDirectory { dir => 70 | val subdir = Files.createDirectories(dir.toPath.resolve("subdir")) 71 | val file = Files.createFile(subdir.resolve("foo.template.scala")).toFile 72 | assert(PathFinder(dir).globRecursive("*.template.scala").get() == Seq(file)) 73 | } 74 | it should "follow links" in IO.withTemporaryDirectory { dir => 75 | IO.withTemporaryDirectory { otherDir => 76 | val foo = Files.createTempFile(otherDir.toPath, "foo", "") 77 | val link = Files.createSymbolicLink(dir.toPath.resolve("link"), otherDir.toPath) 78 | dir.all.toSet shouldBe Set(dir, link.toFile, link.resolve(foo.getFileName).toFile) 79 | } 80 | } 81 | it should "include the base directory" in IO.withTemporaryDirectory { dir => 82 | val file = Files.createFile(dir.toPath.resolve("file")).toFile 83 | dir.all.toSet shouldBe Set(dir, file) 84 | } 85 | it should "preserve ordering" in IO.withTemporaryDirectory { dir => 86 | val subdir = Files.createDirectories(dir.toPath.resolve("subdir")) 87 | val file = Files.createFile(subdir.resolve("file")) 88 | assert(dir.all == dir +: Seq(subdir, file).map(_.toFile)) 89 | } 90 | } 91 | class NioPathFinderSpec extends PathFinderSpec { 92 | override def handler: (File, FileFilter, mutable.Set[File]) => Unit = 93 | DescendantOrSelfPathFinder.default(_, _, _, Int.MaxValue) 94 | } 95 | class NativePathFinderSpec extends PathFinderSpec { 96 | override def handler: (File, FileFilter, mutable.Set[File]) => Unit = 97 | DescendantOrSelfPathFinder.native(_, _, _, Int.MaxValue) 98 | } 99 | -------------------------------------------------------------------------------- /io/src/test/scala/sbt/io/PathMapperSpec.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * sbt IO 3 | * Copyright Scala Center, Lightbend, and Mark Harrah 4 | * 5 | * Licensed under Apache License 2.0 6 | * SPDX-License-Identifier: Apache-2.0 7 | * 8 | * See the NOTICE file distributed with this work for 9 | * additional information regarding copyright ownership. 10 | */ 11 | 12 | package sbt.io 13 | 14 | import java.nio.file.{ Files, Path => NioPath } 15 | 16 | import org.scalatest.Outcome 17 | import org.scalatest.flatspec 18 | import org.scalatest.matchers.should.Matchers 19 | import Path._ 20 | import sbt.io.syntax._ 21 | 22 | class PathMapperSpec extends flatspec.FixtureAnyFlatSpec with Matchers { 23 | 24 | type FixtureParam = NioPath 25 | 26 | "rebase | flat" should "copy resource mappings correctly" in { tempDirectory => 27 | val base = tempDirectory.toFile 28 | 29 | val files = Seq(base / "src" / "main" / "resources" / "scalac-plugin.xml") 30 | val dirs = Seq( 31 | base / "src" / "main" / "resources", 32 | base / "target" / "scala-2.11" / "resource_managed" / "main" 33 | ) 34 | val target = base / "target" / "scala-2.11" / "classes" 35 | 36 | val mappings = (files --- dirs) pair (file => 37 | rebase(dirs, target)(file) orElse (flat(target): File => Option[File])(file) 38 | ) 39 | 40 | mappings shouldBe Seq( 41 | base / "src" / "main" / "resources" / "scalac-plugin.xml" -> 42 | base / "target" / "scala-2.11" / "classes" / "scalac-plugin.xml" 43 | ) 44 | } 45 | 46 | "directory" should "create mappings including the baseDirectory" in { tempDirectory => 47 | val nestedFile1 = Files.createFile(tempDirectory resolve "file1").toFile 48 | val nestedFile2 = Files.createFile(tempDirectory resolve "file2").toFile 49 | val nestedDir = Files.createDirectory(tempDirectory resolve "dir1") 50 | val nestedDirFile = Files.createDirectory(nestedDir resolve "dir1-file1").toFile 51 | 52 | IO.touch(nestedFile1) 53 | IO.touch(nestedFile2) 54 | IO.createDirectory(nestedDir.toFile) 55 | IO.touch(nestedDirFile) 56 | 57 | val mappings = Path.directory(tempDirectory.toFile).map { case (f, s) => (f, file(s)) } 58 | 59 | mappings should contain theSameElementsAs List( 60 | tempDirectory.toFile -> file(s"${tempDirectory.getFileName}"), 61 | nestedFile1 -> file(s"${tempDirectory.getFileName}/file1"), 62 | nestedFile2 -> file(s"${tempDirectory.getFileName}/file2"), 63 | nestedDir.toFile -> file(s"${tempDirectory.getFileName}/dir1"), 64 | nestedDirFile -> file(s"${tempDirectory.getFileName}/dir1/dir1-file1") 65 | ) 66 | } 67 | 68 | it should "create one mapping entry for an empty directory" in { tempDirectory => 69 | val mappings = Path.directory(tempDirectory.toFile) 70 | 71 | mappings should contain theSameElementsAs List[(File, String)]( 72 | tempDirectory.toFile -> s"${tempDirectory.getFileName}" 73 | ) 74 | } 75 | 76 | it should "create an empty mappings sequence for a non-existing directory" in { tempDirectory => 77 | val nonExistingDirectory = tempDirectory.resolve("imaginary") 78 | val mappings = Path.directory(nonExistingDirectory.toFile) 79 | 80 | mappings should be(empty) 81 | } 82 | 83 | it should "create one mapping entry if the directory is a file" in { tempDirectory => 84 | val file = tempDirectory.resolve("file").toFile 85 | IO.touch(file) 86 | val mappings = Path.directory(file) 87 | 88 | mappings should contain theSameElementsAs List[(File, String)]( 89 | file -> s"${file.getName}" 90 | ) 91 | } 92 | 93 | "contentOf" should "create mappings excluding the baseDirectory" in { tempDirectory => 94 | val nestedFile1 = Files.createFile(tempDirectory resolve "file1").toFile 95 | val nestedFile2 = Files.createFile(tempDirectory resolve "file2").toFile 96 | val nestedDir = Files.createDirectory(tempDirectory resolve "dir1") 97 | val nestedDirFile = Files.createDirectory(nestedDir resolve "dir1-file1").toFile 98 | 99 | IO.touch(nestedFile1) 100 | IO.touch(nestedFile2) 101 | IO.createDirectory(nestedDir.toFile) 102 | IO.touch(nestedDirFile) 103 | 104 | val mappings = Path.contentOf(tempDirectory.toFile).map { case (f, s) => (f, file(s)) } 105 | 106 | mappings should contain theSameElementsAs List( 107 | nestedFile1 -> file(s"file1"), 108 | nestedFile2 -> file(s"file2"), 109 | nestedDir.toFile -> file(s"dir1"), 110 | nestedDirFile -> file(s"dir1/dir1-file1") 111 | ) 112 | } 113 | 114 | it should "create an empty mappings sequence for an empty directory" in { tempDirectory => 115 | val mappings = Path.contentOf(tempDirectory.toFile) 116 | 117 | mappings should be(empty) 118 | } 119 | 120 | it should "create an empty mappings sequence for a non-existing directory" in { tempDirectory => 121 | val nonExistingDirectory = tempDirectory.resolve("imaginary") 122 | val mappings = Path.contentOf(nonExistingDirectory.toFile) 123 | 124 | mappings should be(empty) 125 | } 126 | 127 | it should "create an empty mappings sequence if the directory is a file" in { tempDirectory => 128 | val file = tempDirectory.resolve("file").toFile 129 | val mappings = Path.contentOf(file) 130 | 131 | mappings should be(empty) 132 | } 133 | 134 | it should "not include the base directory" in { tempDirectory => 135 | val file = Files.createFile(tempDirectory.resolve("file")) 136 | val paths = Path.allSubpaths(tempDirectory.toFile).toVector.map(_._1.toPath).toSet 137 | assert(paths.contains(file)) 138 | assert(!paths.contains(tempDirectory)) 139 | } 140 | 141 | override protected def withFixture(test: OneArgTest): Outcome = { 142 | val tmpDir = Files.createTempDirectory("path-mappings") 143 | try { 144 | withFixture(test.toNoArgTest(tmpDir)) 145 | } finally { 146 | // cleanup an delete the temp directory 147 | IO.delete(tmpDir.toFile) 148 | } 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /io/src/test/scala/sbt/io/StashSpec.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * sbt IO 3 | * Copyright Scala Center, Lightbend, and Mark Harrah 4 | * 5 | * Licensed under Apache License 2.0 6 | * SPDX-License-Identifier: Apache-2.0 7 | * 8 | * See the NOTICE file distributed with this work for 9 | * additional information regarding copyright ownership. 10 | */ 11 | 12 | package sbt.io 13 | 14 | import java.io.File 15 | import org.scalatest.flatspec.AnyFlatSpec 16 | import org.scalatest.matchers.should.Matchers 17 | 18 | class StashSpec extends AnyFlatSpec with Matchers { 19 | "stash" should "handle empty files" in { 20 | IO.stash(Set()) {} 21 | succeed 22 | } 23 | 24 | it should "move files during execution" in WithFiles(TestFiles*)(checkMove) 25 | it should "restore files on exceptions but not errors" in WithFiles(TestFiles*)(checkRestore) 26 | 27 | def checkRestore(seq: Seq[File]): Unit = { 28 | allCorrect(seq) 29 | 30 | stash0(seq, throw new TestRuntimeException) shouldBe false 31 | allCorrect(seq) 32 | 33 | stash0(seq, throw new TestException) shouldBe false 34 | allCorrect(seq) 35 | 36 | stash0(seq, throw new TestError) shouldBe false 37 | noneExist(seq) 38 | } 39 | 40 | def checkMove(seq: Seq[File]): Unit = { 41 | allCorrect(seq) 42 | assert(stash0(seq, ())) 43 | noneExist(seq) 44 | } 45 | 46 | def stash0(seq: Seq[File], post: => Unit): Boolean = 47 | try { 48 | IO.stash(Set() ++ seq) { 49 | noneExist(seq) 50 | post 51 | } 52 | true 53 | } catch { 54 | case _: TestError | _: TestException | _: TestRuntimeException => false 55 | } 56 | 57 | def allCorrect(s: Seq[File]): Unit = (s.toList zip TestFiles.toList).foreach((correct _).tupled) 58 | 59 | def correct(check: File, ref: (File, String)): Unit = { 60 | assert(check.exists) 61 | IO.read(check) shouldBe ref._2 62 | () 63 | } 64 | 65 | def noneExist(s: Seq[File]): Unit = { 66 | s.forall(!_.exists) shouldBe true 67 | () 68 | } 69 | 70 | val TestFiles = Seq( 71 | "a/b/c" -> "content1", 72 | "a/b/e" -> "content1", 73 | "c" -> "", 74 | "e/g" -> "asdf", 75 | "a/g/c" -> "other" 76 | ) map { case (f, c) => (new File(f), c) } 77 | } 78 | 79 | class TestError extends Error 80 | class TestRuntimeException extends RuntimeException 81 | class TestException extends Exception 82 | -------------------------------------------------------------------------------- /io/src/test/scala/sbt/io/WithFiles.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * sbt IO 3 | * Copyright Scala Center, Lightbend, and Mark Harrah 4 | * 5 | * Licensed under Apache License 2.0 6 | * SPDX-License-Identifier: Apache-2.0 7 | * 8 | * See the NOTICE file distributed with this work for 9 | * additional information regarding copyright ownership. 10 | */ 11 | 12 | package sbt.io 13 | 14 | import java.io.File 15 | 16 | object WithFiles { 17 | 18 | /** 19 | * Takes the relative path -> content pairs and writes the content to a file in a temporary directory. 20 | * The written file path is the relative path resolved against the temporary directory path. 21 | * The provided function is called with the resolved file paths in the same order as the inputs. 22 | */ 23 | def apply[T](sources: (File, String)*)(f: Seq[File] => T): T = 24 | IO.withTemporaryDirectory { dir => 25 | val sourceFiles = 26 | for ((file, content) <- sources) yield { 27 | assert(!file.isAbsolute) 28 | val to = new File(dir, file.getPath) 29 | IO.write(to, content) 30 | to 31 | } 32 | f(sourceFiles) 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /io/src/test/scala/sbt/nio/FileAttributeSpec.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * sbt IO 3 | * Copyright Scala Center, Lightbend, and Mark Harrah 4 | * 5 | * Licensed under Apache License 2.0 6 | * SPDX-License-Identifier: Apache-2.0 7 | * 8 | * See the NOTICE file distributed with this work for 9 | * additional information regarding copyright ownership. 10 | */ 11 | 12 | package sbt.nio 13 | 14 | import java.nio.file.Files 15 | 16 | import org.scalatest.flatspec.AnyFlatSpec 17 | import sbt.io.IO 18 | import sbt.nio.file.FileAttributes 19 | 20 | class FileAttributeSpec extends AnyFlatSpec { 21 | "symlinks" should "be resolved" in IO.withTemporaryDirectory { dir => 22 | val dirPath = dir.toPath 23 | val file = Files.createFile(dirPath.resolve("file")) 24 | val link = Files.createSymbolicLink(dirPath.resolve("link"), file) 25 | FileAttributes(link).toOption match { 26 | case None => assert(false) 27 | case Some(a) => 28 | assert(a.isRegularFile) 29 | assert(a.isSymbolicLink) 30 | assert(!a.isDirectory) 31 | assert(!a.isOther) 32 | } 33 | } 34 | "broken symlinks" should "have isSymbolicLink return true" in IO.withTemporaryDirectory { dir => 35 | val dirPath = dir.toPath 36 | val file = dirPath.resolve("file") 37 | val link = Files.createSymbolicLink(dirPath.resolve("link"), file) 38 | FileAttributes(link).toOption match { 39 | case None => assert(false) 40 | case Some(a) => 41 | assert(a.isSymbolicLink) 42 | assert(!a.isRegularFile) 43 | assert(!a.isDirectory) 44 | assert(!a.isOther) 45 | } 46 | } 47 | "symlinks" should "not be resolved with nofollow links" in IO.withTemporaryDirectory { dir => 48 | val dirPath = dir.toPath 49 | val subdir = Files.createFile(dirPath.resolve("file")) 50 | val link = Files.createSymbolicLink(dirPath.resolve("link"), subdir) 51 | FileAttributes(link, followLinks = false).toOption match { 52 | case None => assert(false) 53 | case Some(a) => 54 | assert(a.isSymbolicLink) 55 | assert(!a.isRegularFile) 56 | assert(!a.isDirectory) 57 | assert(!a.isOther) 58 | } 59 | } 60 | "broken symlinks" should "have isSymbolicLink true with no follow" in IO.withTemporaryDirectory { 61 | dir => 62 | val dirPath = dir.toPath 63 | val file = dirPath.resolve("file") 64 | val link = Files.createSymbolicLink(dirPath.resolve("link"), file) 65 | FileAttributes(link, followLinks = false).toOption match { 66 | case None => assert(false) 67 | case Some(a) => 68 | assert(a.isSymbolicLink) 69 | assert(!a.isRegularFile) 70 | assert(!a.isDirectory) 71 | assert(!a.isOther) 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /io/src/test/scala/sbt/nio/FileTreeViewSpec.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * sbt IO 3 | * Copyright Scala Center, Lightbend, and Mark Harrah 4 | * 5 | * Licensed under Apache License 2.0 6 | * SPDX-License-Identifier: Apache-2.0 7 | * 8 | * See the NOTICE file distributed with this work for 9 | * additional information regarding copyright ownership. 10 | */ 11 | 12 | package sbt.nio 13 | 14 | import java.nio.file._ 15 | 16 | import org.scalatest.flatspec.AnyFlatSpec 17 | import sbt.io.IO 18 | import sbt.nio.file.{ 19 | **, 20 | AnyPath, 21 | IsDirectory, 22 | FileAttributes, 23 | FileTreeView, 24 | Glob, 25 | RecursiveGlob, 26 | IsRegularFile 27 | } 28 | import sbt.nio.file.syntax._ 29 | 30 | import scala.collection.mutable 31 | 32 | class FileTreeViewSpec extends AnyFlatSpec { 33 | val view = FileTreeView.default 34 | "list" should "return the source root with depth == -1" in IO.withTemporaryDirectory { dir => 35 | assert(view.list(dir.toPath.getParent).filter(_._1 == dir.toPath).map(_._1) == Seq(dir.toPath)) 36 | } 37 | it should "not return the source root with depth >= 0" in IO.withTemporaryDirectory { dir => 38 | assert(view.list(Glob(dir, AnyPath / RecursiveGlob)).isEmpty) 39 | } 40 | it should "get recursive files" in IO.withTemporaryDirectory { dir => 41 | val subdir = Files.createDirectory(dir.toPath.resolve("subdir")) 42 | val nestedSubdir = Files.createDirectory(subdir.resolve("nested-subdir")) 43 | val file = Files.createFile(nestedSubdir.resolve("file")) 44 | assert( 45 | view.list(Glob(dir, RecursiveGlob)).collect { case (p, a) if !a.isDirectory => p } == Seq( 46 | file 47 | ) 48 | ) 49 | } 50 | it should "handle multiple globs" in IO.withTemporaryDirectory { dir => 51 | val srcSubdir = Files.createDirectory(dir.toPath.resolve("src")) 52 | val mainSubdir = Files.createDirectory(srcSubdir.resolve("main")) 53 | val scalaSubdir = Files.createDirectory(mainSubdir.resolve("scala")) 54 | val exampleSubdir = Files.createDirectory(scalaSubdir.resolve("example")) 55 | val fooFile = Files.createFile(exampleSubdir.resolve("foo.scala")) 56 | val elevenSubdir = Files.createDirectory(mainSubdir.resolve("scala_2.11+")) 57 | val barFile = Files.createFile(elevenSubdir.resolve("bar.scala")) 58 | val jvmGlob = Glob(dir, "jvm") 59 | val srcGlob = Glob(dir, "src") 60 | 61 | val xs = FileTreeView 62 | .Ops(FileTreeView.default) 63 | .list( 64 | List( 65 | jvmGlob / "src" / "main" / "scala-2.12" / RecursiveGlob / "*.{scala,java}", 66 | jvmGlob / "src" / "main" / "scala" / RecursiveGlob / "*.{scala,java}", 67 | jvmGlob / "src" / "main" / "java" / RecursiveGlob / "*.{scala,java}", 68 | jvmGlob / "src" / "main" / "java" / RecursiveGlob / "*.txt", 69 | srcGlob / "main" / "scala-2.12" / RecursiveGlob / "*.{scala,java}", 70 | srcGlob / "main" / "scala" / RecursiveGlob / "*.{scala,java}", 71 | srcGlob / "main" / "scala_2.11+" / RecursiveGlob / "*.{scala,java}", 72 | srcGlob / "main" / "scala_2.13-" / RecursiveGlob / "*.{scala,java}", 73 | jvmGlob / AnyPath / "*.{scala,java}" 74 | ) 75 | ) 76 | .map { case (p, _) => p } 77 | assert(xs.contains(fooFile) && xs.contains(barFile)) 78 | } 79 | it should "list directories only once" in IO.withTemporaryDirectory { dir => 80 | val file1 = Files.createFile(dir.toPath.resolve("file-1")) 81 | val file2 = Files.createFile(dir.toPath.resolve("file-2")) 82 | val listed = new mutable.ArrayBuffer[Path] 83 | val view: FileTreeView.Nio[FileAttributes] = (path: Path) => { 84 | val res = FileTreeView.default.list(path) 85 | listed += path 86 | res 87 | } 88 | val paths = view.list(Seq(Glob(file1), Glob(file2))).map(_._1) 89 | assert(paths.toSet == Set(file1, file2)) 90 | assert(listed == Seq(file1.getParent)) 91 | } 92 | it should "apply filters" in IO.withTemporaryDirectory { dir => 93 | val subdir = Files.createDirectories(dir.toPath.resolve("subdir")) 94 | val nested = Files.createDirectories(subdir.resolve("nested")) 95 | val subdirFile = Files.createFile(subdir.resolve("file")) 96 | val nestedFile = Files.createFile(nested.resolve("file")) 97 | val glob = Glob(dir.toPath, RecursiveGlob) 98 | 99 | val files = FileTreeView.default.list(glob, IsRegularFile) 100 | assert(files.map(_._1) == Seq(subdirFile, nestedFile)) 101 | 102 | val directories = FileTreeView.default.list(glob, IsDirectory) 103 | assert(directories.map(_._1) == Seq(subdir, nested)) 104 | } 105 | it should "handle exact file glob" in IO.withTemporaryDirectory { dir => 106 | val file = Files.createFile(dir.toPath.resolve("file")) 107 | assert(FileTreeView.default.list(file.toGlob).map(_._1) == Seq(file)) 108 | } 109 | it should "handle many exact file globs" in IO.withTemporaryDirectory { dir => 110 | val random = new scala.util.Random() 111 | val n = 2000 112 | val nested = Files.createDirectories(dir.toPath / "subdir" / "nested") 113 | // TODO https://github.com/lampepfl/dotty/issues/13941 114 | // @tailrec 115 | def newRandomFile(): Path = 116 | try Files.createFile(nested / s"a${1 + random.nextInt(n)}") 117 | catch { case _: FileAlreadyExistsException => newRandomFile() } 118 | val randomFiles = (1 to Seq(10, n).min).map(_ => newRandomFile()) 119 | val globs = (1 to n).map(i => dir.toGlob / "subdir" / "nested" / s"a$i") :+ Glob(dir, ** / "b*") 120 | assert(randomFiles.toSet == FileTreeView.default.list(globs).map(_._1).toSet) 121 | } 122 | it should "handle overlapping globs with exact file" in IO.withTemporaryDirectory { dir => 123 | val subdir = Files.createDirectories(dir.toPath / "subdir") 124 | val nested = Files.createDirectories(subdir / "nested") 125 | val a = Files.createFile(subdir / "a.txt") 126 | val b = Files.createFile(nested / "b.txt") 127 | val globs = Seq(Glob(subdir, "a.txt"), Glob(nested, RecursiveGlob)) 128 | val queried = FileTreeView.default.list(globs).map(_._1).toSet 129 | assert(queried == Set(a, b)) 130 | } 131 | it should "throw NoSuchFileException for non-existent directories" in IO.withTemporaryDirectory { 132 | dir => 133 | intercept[NoSuchFileException](FileTreeView.nio.list(dir.toPath / "foo")) 134 | intercept[NoSuchFileException](FileTreeView.native.list(dir.toPath / "foo")) 135 | } 136 | it should "throw NotDirectoryException for regular files" in IO.withTemporaryDirectory { dir => 137 | val file = Files.createFile(dir.toPath / "file") 138 | intercept[NotDirectoryException](FileTreeView.nio.list(file)) 139 | intercept[NotDirectoryException](FileTreeView.native.list(file)) 140 | } 141 | "iterator" should "be lazy" in IO.withTemporaryDirectory { dir => 142 | val firstSubdir = Files.createDirectory(dir.toPath.resolve("first")) 143 | val firstSubdirFile = Files.createFile(firstSubdir.resolve("first-file")) 144 | val firstSubdirOtherFile = Files.createFile(firstSubdir.resolve("second-file")) 145 | val secondSubdir = Files.createDirectory(firstSubdir.resolve("second")) 146 | val secondSubdirFile = Files.createFile(secondSubdir.resolve("file")) 147 | object ListingFileTreeView extends FileTreeView.Nio[FileAttributes] { 148 | val listed = mutable.Set.empty[Path] 149 | override def list(path: Path): Seq[(Path, FileAttributes)] = { 150 | listed += path 151 | FileTreeView.default.list(path) 152 | } 153 | } 154 | val firstSubdirFoundFile = 155 | ListingFileTreeView.iterator(Glob(dir, RecursiveGlob)).collectFirst { 156 | case (p, a) if a.isRegularFile => p 157 | } 158 | assert(firstSubdirFoundFile.map(_.getParent).contains(firstSubdir)) 159 | assert(ListingFileTreeView.listed.toSet == Set(dir.toPath, firstSubdir)) 160 | val allFiles = ListingFileTreeView 161 | .iterator(Glob(dir, RecursiveGlob)) 162 | .collect { 163 | case (p, a) if a.isRegularFile => p 164 | } 165 | .toSet 166 | assert(allFiles == Set(firstSubdirFile, firstSubdirOtherFile, secondSubdirFile)) 167 | assert(ListingFileTreeView.listed.toSet == Set(dir.toPath, firstSubdir, secondSubdir)) 168 | } 169 | } 170 | -------------------------------------------------------------------------------- /io/src/test/scala/sbt/nio/GlobFilterSpec.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * sbt IO 3 | * Copyright Scala Center, Lightbend, and Mark Harrah 4 | * 5 | * Licensed under Apache License 2.0 6 | * SPDX-License-Identifier: Apache-2.0 7 | * 8 | * See the NOTICE file distributed with this work for 9 | * additional information regarding copyright ownership. 10 | */ 11 | 12 | package sbt.nio 13 | 14 | import java.nio.file.Paths 15 | 16 | import org.scalatest.flatspec.AnyFlatSpec 17 | import sbt.io.IO 18 | import sbt.io.syntax._ 19 | import sbt.nio.file.{ AnyPath, Glob, RecursiveGlob } 20 | import sbt.nio.file.Glob.GlobOps 21 | 22 | class GlobFilterSpec extends AnyFlatSpec { 23 | "GlobAsFilter" should "work with simple files" in IO.withTemporaryDirectory { dir => 24 | val file = new File(dir, "file") 25 | val nestedFile = new File(new File(dir, "subdir"), "subdir-file") 26 | val filter = Glob(dir).toFileFilter 27 | assert(filter.accept(dir)) 28 | assert(!filter.accept(file)) 29 | assert(!filter.accept(nestedFile)) 30 | } 31 | it should "work with globs" in IO.withTemporaryDirectory { dir => 32 | val file = new File(dir, "file") 33 | val nestedFile = new File(new File(dir, "subdir"), "subdir-file") 34 | val glob = Glob(dir, AnyPath) 35 | assert(!glob.matches(dir.toPath)) 36 | assert(glob.matches(file.toPath)) 37 | assert(!glob.matches(nestedFile.toPath)) 38 | } 39 | it should "work with recursive globs" in IO.withTemporaryDirectory { dir => 40 | val file = new File(dir, "file") 41 | val nestedFile = new File(new File(dir, "subdir"), "subdir-file") 42 | val glob = Glob(dir, RecursiveGlob) 43 | assert(!glob.matches(dir.toPath)) 44 | assert(glob.matches(file.toPath)) 45 | assert(glob.matches(nestedFile.toPath)) 46 | } 47 | it should "work with depth" in { 48 | val base = Paths.get("").toAbsolutePath.getRoot.resolve("foo").resolve("bar") 49 | assert(Glob(base, s"*/*.txt").matches(base.resolve("foo").resolve("bar.txt"))) 50 | assert(!Glob(base, s"*/*/*.txt").matches(base.resolve("foo").resolve("bar.txt"))) 51 | assert(Glob(base, s"*/*/*.txt").matches(base.resolve("foo").resolve("baz").resolve("bar.txt"))) 52 | assert(!Glob(base, s"*/*/*.txt").matches(base.resolve("foo").resolve("baz").resolve("bar.tx"))) 53 | assert(Glob(base, s"*/**/*.txt").matches(base.resolve("foo").resolve("bar.txt"))) 54 | assert( 55 | Glob(base, s"*/**/*.txt") 56 | .matches(base.resolve("foo").resolve("bar").resolve("baz").resolve("bar.txt")) 57 | ) 58 | assert( 59 | !Glob(base, s"*/**/*.txt") 60 | .matches(base.resolve("foo").resolve("bar").resolve("baz").resolve("bar.tx")) 61 | ) 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /io/src/test/scala/sbt/nio/GlobOrderingSpec.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * sbt IO 3 | * Copyright Scala Center, Lightbend, and Mark Harrah 4 | * 5 | * Licensed under Apache License 2.0 6 | * SPDX-License-Identifier: Apache-2.0 7 | * 8 | * See the NOTICE file distributed with this work for 9 | * additional information regarding copyright ownership. 10 | */ 11 | 12 | package sbt.nio 13 | 14 | import java.io.File 15 | 16 | import org.scalatest.flatspec.AnyFlatSpec 17 | import sbt.io.{ IO, SimpleFilter } 18 | import sbt.nio.file.RelativeGlob.Matcher 19 | import sbt.nio.file._ 20 | import sbt.nio.file.syntax._ 21 | 22 | import scala.collection.JavaConverters._ 23 | 24 | class GlobOrderingSpec extends AnyFlatSpec { 25 | "Globs" should "be ordered" in IO.withTemporaryDirectory { dir => 26 | val subdir = new File(dir, "subdir") 27 | assert(Seq(Glob(subdir), Glob(dir)).sorted == Seq(Glob(dir), Glob(subdir))) 28 | } 29 | they should "fall back on depth" in IO.withTemporaryDirectory { dir => 30 | val recursive = Glob(dir, **) 31 | val nonRecursive = Glob(dir) 32 | assert(Seq(nonRecursive, recursive).sorted == Seq(recursive, nonRecursive)) 33 | } 34 | they should "not stack overflow" in IO.withTemporaryDirectory { dir => 35 | val exact = Glob(dir.toPath.resolve("foo")) 36 | val fullFile = 37 | sbt.internal.nio.Globs(dir.toPath / "foo", recursive = true, sbt.io.HiddenFileFilter) 38 | assert(Seq(exact, fullFile).sorted == Seq(exact, fullFile)) 39 | } 40 | they should "not violate sorting contract" in IO.withTemporaryDirectory { dir => 41 | val globs = Seq( 42 | **, 43 | ** / "foo", 44 | ** / * / "bar", 45 | ** / "foo" / "bar", 46 | ** / Matcher.or(Matcher("foo"), Matcher("bar")), 47 | ** / Matcher.and(Matcher("foo"), Matcher("bar")), 48 | ** / Matcher.not(Matcher("foo")), 49 | ** / Matcher.not(Matcher.and(Matcher("foo"), Matcher("bar"))), 50 | ** / Matcher(_.contains("foo")), 51 | ** / "foo" / Matcher(_.contains("bar")), 52 | ** / "foo" / Matcher.not(Matcher(_.contains("bar"))), 53 | ** / "foo" / "*.scala", 54 | ** / **, 55 | ** / *, 56 | (** / "foo") / * / "*.scala", 57 | (** / "foo") / * / "*.scala*", 58 | (** / "foo") / ** / "*.scala*", 59 | Glob(dir.toPath.resolve("foo")), 60 | Glob(dir.toPath.resolve("bar")), 61 | Glob(dir.toPath.resolve("bar").resolve("baz")), 62 | sbt.internal.nio.Globs(dir.toPath, recursive = false, sbt.io.AllPassFilter), 63 | sbt.internal.nio.Globs(dir.toPath, recursive = false, new SimpleFilter(_.contains("bar"))), 64 | sbt.internal.nio.Globs(dir.toPath, recursive = true, new SimpleFilter(_.contains("baz"))), 65 | sbt.internal.nio.Globs(dir.toPath, recursive = true, sbt.io.HiddenFileFilter), 66 | sbt.internal.nio.Globs(dir.toPath / "foo", recursive = true, sbt.io.HiddenFileFilter), 67 | sbt.internal.nio.Globs(dir.toPath, recursive = true, sbt.io.NothingFilter), 68 | sbt.internal.nio.Globs(dir.toPath, recursive = true, new SimpleFilter(_.contains("foo"))), 69 | Glob(dir.toPath / "scala", ** / "*.scala"), 70 | Glob(dir.toPath / "java", ** / "*.java"), 71 | Glob(dir.toPath / "scala", ** / "*.java"), 72 | ) 73 | val javaGlobs = new java.util.ArrayList((globs ++ globs ++ globs).asJava) 74 | 1 to 1000 foreach { _ => 75 | java.util.Collections.shuffle(javaGlobs) 76 | javaGlobs.asScala.sorted 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /io/src/test/scala/sbt/nio/GlobParserSpec.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * sbt IO 3 | * Copyright Scala Center, Lightbend, and Mark Harrah 4 | * 5 | * Licensed under Apache License 2.0 6 | * SPDX-License-Identifier: Apache-2.0 7 | * 8 | * See the NOTICE file distributed with this work for 9 | * additional information regarding copyright ownership. 10 | */ 11 | 12 | package sbt.nio 13 | 14 | import org.scalatest.flatspec.AnyFlatSpec 15 | import sbt.nio.TestHelpers._ 16 | import sbt.nio.file.{ AnyPath, Glob, RecursiveGlob, RelativeGlob } 17 | 18 | class GlobParserSpec extends AnyFlatSpec { 19 | it should "parse pure paths" in { 20 | assert(Glob(s"$basePath/baz") == Glob(basePath.resolve("baz"))) 21 | val absolute = Glob(s"$basePath/baz") 22 | assert(absolute == Glob(basePath.resolve("baz"))) 23 | val relative = Glob(s"bar/baz") 24 | assert(relative == RelativeGlob("bar", "baz")) 25 | } 26 | it should "parse paths with range" in { 27 | val children = Glob(s"$basePath/*") 28 | assert(children == Glob(basePath, AnyPath)) 29 | val subChildren = Glob(s"$basePath/*/*") 30 | assert(subChildren == Glob(basePath, AnyPath / AnyPath)) 31 | val recursive = Glob(s"$basePath/**") 32 | assert(recursive == Glob(basePath, RecursiveGlob)) 33 | val recursiveSubchildren = Glob(s"$basePath/*/*/**") 34 | assert(recursiveSubchildren == Glob(basePath, AnyPath / AnyPath / RecursiveGlob)) 35 | } 36 | it should "parse paths with filters" in { 37 | val exact = Glob(s"$basePath/*/foo.txt") 38 | assert(exact == Glob(basePath, AnyPath / "foo.txt")) 39 | val extension = Glob(s"$basePath/**/*.s") 40 | assert(extension == Glob(basePath, RecursiveGlob / "*.s")) 41 | val prefix = Glob(s"foo/bar/*/*/foo*") 42 | assert(prefix == RelativeGlob("foo", "bar") / AnyPath / AnyPath / "foo*") 43 | val suffix = Glob(s"foo/bar/*/*/*bar") 44 | assert(suffix == RelativeGlob("foo", "bar") / AnyPath / AnyPath / "*bar") 45 | val prefixAndSuffix = Glob(s"$basePath/*/**/foo*bar") 46 | assert(prefixAndSuffix == Glob(basePath) / AnyPath / RecursiveGlob / "foo*bar") 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /io/src/test/scala/sbt/nio/PathFilterSpec.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * sbt IO 3 | * Copyright Scala Center, Lightbend, and Mark Harrah 4 | * 5 | * Licensed under Apache License 2.0 6 | * SPDX-License-Identifier: Apache-2.0 7 | * 8 | * See the NOTICE file distributed with this work for 9 | * additional information regarding copyright ownership. 10 | */ 11 | 12 | package sbt.nio 13 | 14 | import java.nio.file.{ Files, Path } 15 | 16 | import org.scalatest.flatspec.AnyFlatSpec 17 | import sbt.io.IO 18 | import sbt.nio.file._ 19 | import sbt.nio.file.syntax._ 20 | 21 | object PathFilterSpec { 22 | implicit class PathFilterOps(val pathFilter: PathFilter) extends AnyVal { 23 | def accept(path: Path): Boolean = FileAttributes(path) match { 24 | case Left(_) => pathFilter.accept(path, FileAttributes.NonExistent) 25 | case Right(a) => pathFilter.accept(path, a) 26 | } 27 | } 28 | private val isWin = scala.util.Properties.isWin 29 | implicit class PathOps(val path: Path) extends AnyVal { 30 | def setHidden(): Path = 31 | if (isWin) Files.setAttribute(path, "dos:hidden", true) else path 32 | } 33 | } 34 | import sbt.nio.PathFilterSpec._ 35 | class PathFilterSpec extends AnyFlatSpec { 36 | "PathFilters" should "accept files" in IO.withTemporaryDirectory { dir => 37 | val dirPath = dir.toPath 38 | val foo = Files.createFile(dirPath / "foo") 39 | val fooTxt = dirPath / "foo.txt" 40 | val fooScala = Files.createFile(Files.createDirectories(dirPath / "bar") / "foo.scala") 41 | assert(AllPass.accept(foo)) 42 | assert(AllPass.accept(fooTxt)) 43 | assert(AllPass.accept(fooScala)) 44 | assert(!(!AllPass).accept(foo)) 45 | assert(!(!AllPass).accept(fooTxt)) 46 | assert(!(!AllPass).accept(fooScala)) 47 | } 48 | they should "exclude files" in IO.withTemporaryDirectory { dir => 49 | val dirPath = dir.toPath 50 | assert(!NoPass.accept(Files.createFile(dirPath / "foo"))) 51 | assert(!NoPass.accept(dirPath / "foo.txt")) 52 | assert(!NoPass.accept(Files.createFile(Files.createDirectories(dirPath / "bar") / "foo.scala"))) 53 | } 54 | they should "combine filters with &&" in IO.withTemporaryDirectory { dir => 55 | val dirPath = dir.toPath 56 | val hidden = Files.createFile(dirPath / ".hidden.scala").setHidden() 57 | val foo = Files.createFile(dirPath / "foo.scala") 58 | val scalaFilter = PathFilter(dirPath.toGlob / "*.scala") 59 | assert(scalaFilter.accept(hidden)) 60 | assert(scalaFilter.accept(foo)) 61 | assert(!(scalaFilter && IsNotHidden).accept(hidden)) 62 | assert((scalaFilter && IsNotHidden).accept(foo)) 63 | assert(!(scalaFilter && !IsHidden).accept(hidden)) 64 | assert((scalaFilter && !IsHidden).accept(foo)) 65 | assert((scalaFilter && IsHidden).accept(hidden)) 66 | assert(!(scalaFilter && IsHidden).accept(foo)) 67 | } 68 | they should "combine filters with ||" in IO.withTemporaryDirectory { dir => 69 | val dirPath = dir.toPath 70 | val foo = Files.createFile(dirPath / "foo.scala") 71 | val bar = Files.createFile(dirPath / "bar.java") 72 | val scalaFilter = PathFilter(dirPath.toGlob / "*.scala") 73 | val javaFilter = PathFilter(dirPath.toGlob / "*.java") 74 | assert(scalaFilter.accept(foo)) 75 | assert(!scalaFilter.accept(bar)) 76 | assert(!javaFilter.accept(foo)) 77 | assert(javaFilter.accept(bar)) 78 | assert((scalaFilter || javaFilter).accept(foo)) 79 | assert((scalaFilter || javaFilter).accept(bar)) 80 | } 81 | they should "combine glob strings" in IO.withTemporaryDirectory { dir => 82 | val dirPath = dir.toPath 83 | val subdir = Files.createDirectories(dirPath / "subdir") 84 | val nested = Files.createDirectories(subdir / "nested") 85 | val bar = Files.createFile(subdir / "bar.txt") 86 | val baz = Files.createFile(subdir / "abcbazefg") 87 | val foo = Files.createFile(nested / "foo.md") 88 | val filter = AllPass && "**/{*.txt,*baz*}" 89 | assert(FileTreeView.default.list(dirPath.toGlob / **, filter).map(_._1).toSet == Set(bar, baz)) 90 | assert( 91 | FileTreeView.default.list(dirPath.toGlob / **, !filter).map(_._1).toSet == 92 | Set(subdir, nested, foo) 93 | ) 94 | } 95 | they should "combine file filters" in IO.withTemporaryDirectory { dir => 96 | val notHiddenFileFilter: PathFilter = !sbt.io.HiddenFileFilter 97 | val hiddenFileFilter: PathFilter = sbt.io.HiddenFileFilter 98 | val dirPath = dir.toPath 99 | val hidden = Files.createFile(dirPath / ".hidden.scala").setHidden() 100 | val regular = Files.createFile(dirPath / "foo.scala") 101 | assert(hiddenFileFilter.accept(hidden)) 102 | assert((!hiddenFileFilter).accept(regular)) 103 | assert(notHiddenFileFilter.accept(regular)) 104 | assert((!notHiddenFileFilter).accept(hidden)) 105 | 106 | val directoryFilterAndHidden: PathFilter = 107 | sbt.io.DirectoryFilter.toNio && sbt.io.HiddenFileFilter 108 | val hiddenDir = Files.createDirectories(dirPath / ".hidden").setHidden() 109 | assert(directoryFilterAndHidden.accept(hiddenDir) == !isWin) 110 | assert(!directoryFilterAndHidden.accept(dirPath)) 111 | assert(!directoryFilterAndHidden.accept(hidden)) 112 | 113 | val directoryFilterOrHidden: PathFilter = 114 | sbt.io.DirectoryFilter.toNio || sbt.io.HiddenFileFilter 115 | assert(directoryFilterOrHidden.accept(hiddenDir)) 116 | assert(directoryFilterOrHidden.accept(dirPath)) 117 | assert(directoryFilterOrHidden.accept(hidden)) 118 | } 119 | they should "combine Globs" in IO.withTemporaryDirectory { dir => 120 | val dirPath = dir.toPath 121 | val foo = Files.createFile(dirPath / "foo.txt") 122 | val bar = Files.createFile(dirPath / "bar.txt") 123 | val txtGlob = dirPath.toGlob / "*.txt" 124 | val notBar = txtGlob && !bar.toGlob 125 | assert(notBar.accept(foo)) 126 | assert(! { notBar.accept(bar) }) 127 | assert(!(!notBar).accept(foo)) 128 | assert((!notBar).accept(bar)) 129 | } 130 | they should "negate" in IO.withTemporaryDirectory { dir => 131 | val dirPath = dir.toPath 132 | val foo = Files.createFile(dirPath / "foo.txt") 133 | val hidden = Files.createFile(dirPath / ".hidden").setHidden() 134 | val filter = IsDirectory && !IsHidden 135 | val notFilter = !filter 136 | assert(notFilter == (!IsDirectory || IsHidden)) 137 | assert(!filter.accept(foo)) 138 | assert(!filter.accept(hidden)) 139 | assert(filter.accept(dirPath)) 140 | assert(notFilter.accept(foo)) 141 | assert(notFilter.accept(hidden)) 142 | assert(!notFilter.accept(dirPath)) 143 | 144 | val tripleAndFilter = IsDirectory && !IsHidden && !(** / "*.txt") 145 | val notTripleAndFilter = !tripleAndFilter 146 | assert(notTripleAndFilter == (!IsDirectory || IsHidden || (** / "*.txt"))) 147 | assert(!(tripleAndFilter.accept(foo))) 148 | assert(tripleAndFilter.accept(dirPath)) 149 | assert(!(tripleAndFilter.accept(hidden))) 150 | assert(notTripleAndFilter.accept(foo)) 151 | assert(notTripleAndFilter.accept(hidden)) 152 | assert(!(notTripleAndFilter.accept(dirPath))) 153 | 154 | val tripleOrFilter = IsDirectory || IsHidden || (** / "*.txt") 155 | val notTripleOrFilter = !tripleOrFilter 156 | assert(notTripleOrFilter == (!IsDirectory && !IsHidden && !(** / "*.txt"))) 157 | assert(tripleOrFilter.accept(foo)) 158 | assert(tripleOrFilter.accept(hidden)) 159 | assert(tripleOrFilter.accept(dirPath)) 160 | assert(!(notTripleOrFilter.accept(foo))) 161 | assert(!(notTripleOrFilter.accept(hidden))) 162 | assert(!(notTripleOrFilter.accept(dirPath))) 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /io/src/test/scala/sbt/nio/TestHelpers.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * sbt IO 3 | * Copyright Scala Center, Lightbend, and Mark Harrah 4 | * 5 | * Licensed under Apache License 2.0 6 | * SPDX-License-Identifier: Apache-2.0 7 | * 8 | * See the NOTICE file distributed with this work for 9 | * additional information regarding copyright ownership. 10 | */ 11 | 12 | package sbt.nio 13 | 14 | import java.nio.file.FileSystems 15 | 16 | import scala.collection.JavaConverters._ 17 | 18 | object TestHelpers { 19 | val root = FileSystems.getDefault.getRootDirectories.asScala.head 20 | val basePath = root.resolve("foo").resolve("bar") 21 | } 22 | -------------------------------------------------------------------------------- /io/src/test/scala/sbt/nio/TraversableGlobSpec.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * sbt IO 3 | * Copyright Scala Center, Lightbend, and Mark Harrah 4 | * 5 | * Licensed under Apache License 2.0 6 | * SPDX-License-Identifier: Apache-2.0 7 | * 8 | * See the NOTICE file distributed with this work for 9 | * additional information regarding copyright ownership. 10 | */ 11 | 12 | package sbt.nio 13 | 14 | import java.nio.file.Files 15 | 16 | import org.scalatest.flatspec.AnyFlatSpec 17 | import sbt.io.IO 18 | import sbt.nio.file.{ FileTreeView, Glob, RecursiveGlob } 19 | 20 | class TraversableGlobSpec extends AnyFlatSpec { 21 | "Traversable globs" should "collect multiple directories" in { 22 | IO.withTemporaryDirectory { dirFile => 23 | val dir = dirFile.toPath 24 | val subdir = Files.createDirectories(dir.resolve("subdir")) 25 | val otherSubdir = Files.createDirectories(dir.resolve("other-subdir")) 26 | val nestedSubdir = Files.createDirectory(otherSubdir.resolve("nested")) 27 | val subdirs = Seq(subdir, otherSubdir, nestedSubdir) 28 | val files = subdirs.map(d => Files.createFile(d.resolve("file"))) 29 | val globs = subdirs.dropRight(1).map(d => Glob(d, RecursiveGlob)) 30 | 31 | val actual = FileTreeView.default.list(globs).map(_._1).toSet 32 | val expected = (files :+ nestedSubdir).toSet 33 | assert(actual == expected) 34 | } 35 | } 36 | it should "handle redundant globs" in { 37 | IO.withTemporaryDirectory { dirFile => 38 | val dir = dirFile.toPath 39 | val subdir = Files.createDirectories(dir.resolve("subdir")) 40 | val file = Files.createFile(subdir.resolve("file.txt")) 41 | val globs = Seq[Glob](s"$dir/**", s"$dir/**/*.txt", s"$subdir/*/*.txt", Glob(file)) 42 | val actual = FileTreeView.default.list(globs).map(_._1).sorted 43 | val expected = Seq(subdir, file) 44 | assert(actual == expected) 45 | } 46 | } 47 | it should "handle semi-overlapping globs" in { 48 | IO.withTemporaryDirectory { dirFile => 49 | val dir = dirFile.toPath 50 | val subdir = Files.createDirectories(dir.resolve("subdir")) 51 | val nested = Files.createDirectories(subdir.resolve("nested")) 52 | val deeply = Files.createDirectories(nested.resolve("deeply")) 53 | val txtFile = Files.createFile(nested.resolve("file.txt")) 54 | val mdFile = Files.createFile(deeply.resolve("file.md")) 55 | val globs = Seq[Glob](s"$dir/**/*.md", s"$subdir/*.txt", s"$nested/*.md", s"$nested/*.txt") 56 | val actual = FileTreeView.default.list(globs).map(_._1).sorted 57 | val expected = Seq(mdFile, txtFile) 58 | assert(actual == expected) 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /notes/1.0.0.markdown: -------------------------------------------------------------------------------- 1 | ### Features, fixes, changes with compatibility implications 2 | 3 | - IO uses the package name `sbt.io`, but it will be imported under `sbt` in sbt/sbt. 4 | - `Path.relativizeFile(baseFile, file)` is renamed to `IO.relativizeFile(baseFile, file)`. 5 | - `PathFinder.x_!(mapper)` moved to `def pair` on `PathFinder`. 6 | - `PathFinder`'s `***` method is moved to `allPaths` method. 7 | 8 | ### Improvements 9 | 10 | - `Path.directory` and `Path.contentOf` are donated from sbt-native-packager #38 by @muuki88. 11 | - `WatchService` that abstracts `PollingWatchService` and Java NIO. #47 by @Duhemm on behalf of The Scala Center. 12 | - Adds variants of `IO.copyFile` and `IO.copyDirectory` that accept `sbt.io.CopyOptions()`. See below for details. 13 | 14 | ### CopyOptions 15 | 16 | sbt IO 1.0 add variant of `IO.copyFile` and `IO.copyDirectory` that accept `sbt.io.CopyOptions()`. 17 | `CopyOptions()` is an example of pseudo case class similar to the builder pattern. 18 | 19 | ```scala 20 | import sbt.io.{ IO, CopyOptions } 21 | 22 | IO.copyDirectory(source, target) 23 | 24 | // The above is same as the following 25 | IO.copyDirectory(source, target, CopyOptions() 26 | .withOverwrite(false) 27 | .withPreserveLastModified(true) 28 | .withPreserveExecutable(true)) 29 | ``` 30 | 31 | sbt/io#53 by @dwijnand 32 | -------------------------------------------------------------------------------- /project/Dependencies.scala: -------------------------------------------------------------------------------- 1 | import sbt._ 2 | import Keys._ 3 | 4 | object Dependencies { 5 | val scala212 = "2.12.20" 6 | val scala213 = "2.13.18" 7 | val scala3 = "3.3.7" 8 | 9 | val scalaCompiler = Def.setting { 10 | val v = if (scalaBinaryVersion.value == "3") scala213 else scalaVersion.value 11 | "org.scala-lang" % "scala-compiler" % v 12 | } 13 | 14 | val scalaVerify = "com.eed3si9n.verify" %% "verify" % "1.0.0" 15 | val scalaCheck = "org.scalacheck" %% "scalacheck" % "1.19.0" 16 | val scalatest = "org.scalatest" %% "scalatest" % "3.2.19" 17 | val swovalFiles = "com.swoval" % "file-tree-views" % "2.1.12" 18 | } 19 | -------------------------------------------------------------------------------- /project/HouseRulesPluglin.scala: -------------------------------------------------------------------------------- 1 | package iobuild 2 | 3 | import sbt._ 4 | import Keys._ 5 | 6 | object HouseRulesPlugin extends AutoPlugin { 7 | override def requires = plugins.JvmPlugin 8 | override def trigger = allRequirements 9 | 10 | override def projectSettings: Seq[Def.Setting[?]] = baseSettings 11 | 12 | lazy val baseSettings: Seq[Def.Setting[?]] = Seq( 13 | scalacOptions ++= Seq("-encoding", "utf8"), 14 | scalacOptions ++= Seq("-deprecation", "-feature", "-unchecked"), 15 | scalacOptions += "-language:higherKinds", 16 | scalacOptions += "-language:implicitConversions", 17 | scalacOptions ++= "-Xfuture".ifScala213OrMinus.value.toList, 18 | scalacOptions ++= "-Xfatal-warnings" 19 | .ifScala(v => { 20 | sys.props.get("sbt.build.fatal") match { 21 | case Some(_) => java.lang.Boolean.getBoolean("sbt.build.fatal") 22 | case _ => v == 12 23 | } 24 | }) 25 | .value 26 | .toList, 27 | scalacOptions ++= "-Xsource:3".ifScala213OrMinus.value.toList, 28 | scalacOptions ++= "-Wconf:msg=package object inheritance is deprecated:warning".ifScala213OrMinus.value.toList, 29 | scalacOptions ++= "-Yno-adapted-args".ifScala212OrMinus.value.toList, 30 | scalacOptions ++= "-Ywarn-dead-code".ifScala213OrMinus.value.toList, 31 | scalacOptions ++= "-Ywarn-numeric-widen".ifScala213OrMinus.value.toList, 32 | scalacOptions ++= "-Ywarn-value-discard".ifScala213OrMinus.value.toList, 33 | ) ++ Seq(Compile, Test).flatMap(c => 34 | (c / console / scalacOptions) --= Seq("-Ywarn-unused-import", "-Xlint") 35 | ) 36 | 37 | private def scalaPartV = Def setting (CrossVersion partialVersion scalaVersion.value) 38 | 39 | private implicit final class AnyWithIfScala[A](val __x: A) { 40 | def ifScala(p: Long => Boolean) = 41 | Def setting (scalaPartV.value collect { case (2, y) if p(y) => __x }) 42 | def ifScalaLte(v: Long) = ifScala(_ <= v) 43 | def ifScalaGte(v: Long) = 44 | Def.setting( 45 | scalaPartV.value.collect { 46 | case (2, y) if y >= v => __x 47 | case (n, _) if n >= 3 => __x 48 | } 49 | ) 50 | def ifScala212OrMinus = ifScalaLte(12) 51 | def ifScala213OrMinus = ifScalaLte(13) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version=1.11.7 2 | -------------------------------------------------------------------------------- /project/plugins.sbt: -------------------------------------------------------------------------------- 1 | addSbtPlugin("com.github.sbt" % "sbt-dynver" % "5.1.1") 2 | addSbtPlugin("com.github.sbt" % "sbt-pgp" % "2.3.1") 3 | addSbtPlugin("com.typesafe" % "sbt-mima-plugin" % "1.1.4") 4 | addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.5.6") 5 | addSbtPlugin("com.eed3si9n" % "sbt-buildinfo" % "0.13.1") 6 | addSbtPlugin("com.github.sbt" % "sbt-header" % "5.11.0") 7 | addSbtPlugin("org.scala-sbt" % "sbt-contraband" % "0.8.0") 8 | --------------------------------------------------------------------------------