summaryrefslogtreecommitdiff
path: root/core/src/mill/eval/PathRef.scala
blob: 118d98fe4b4de1713e6e3a7568f7e3c181f9a6b6 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
package mill.eval

import java.io.IOException
import java.nio.file.attribute.BasicFileAttributes
import java.nio.file.{FileVisitResult, FileVisitor}
import java.nio.{file => jnio}
import java.security.{DigestOutputStream, MessageDigest}

import upickle.default.{ReadWriter => RW}
import ammonite.ops.Path
import mill.util.{DummyOutputStream, IO, JsonFormatters}


/**
  * A wrapper around `ammonite.ops.Path` that calculates it's hashcode based
  * on the contents of the filesystem underneath it. Used to ensure filesystem
  * changes can bust caches which are keyed off hashcodes.
  */
case class PathRef(path: ammonite.ops.Path, quick: Boolean, sig: Int){
  override def hashCode() = sig
}

object PathRef{
  def apply(path: ammonite.ops.Path, quick: Boolean = false) = {
    val sig = {
      val digest = MessageDigest.getInstance("MD5")
      val digestOut = new DigestOutputStream(DummyOutputStream, digest)
      jnio.Files.walkFileTree(
        path.toNIO,
        java.util.EnumSet.of(jnio.FileVisitOption.FOLLOW_LINKS),
        Integer.MAX_VALUE,
        new FileVisitor[jnio.Path] {
          def preVisitDirectory(dir: jnio.Path, attrs: BasicFileAttributes) = {
            digest.update(dir.toAbsolutePath.toString.getBytes)
            FileVisitResult.CONTINUE
          }

          def visitFile(file: jnio.Path, attrs: BasicFileAttributes) = {
            digest.update(file.toAbsolutePath.toString.getBytes)
            if (quick){
              val value = (path.mtime.toMillis, path.size).hashCode()
              digest.update((value >>> 24).toByte)
              digest.update((value >>> 16).toByte)
              digest.update((value >>> 8).toByte)
              digest.update(value.toByte)
            }else {
              val is = jnio.Files.newInputStream(file)
              IO.stream(is, digestOut)
              is.close()
            }
            FileVisitResult.CONTINUE
          }

          def visitFileFailed(file: jnio.Path, exc: IOException) = FileVisitResult.CONTINUE
          def postVisitDirectory(dir: jnio.Path, exc: IOException) = FileVisitResult.CONTINUE
        }
      )

      java.util.Arrays.hashCode(digest.digest())

    }
    new PathRef(path, quick, sig)
  }

  implicit def jsonFormatter: RW[PathRef] = upickle.default.readwriter[String].bimap[PathRef](
    p => {
      (if (p.quick) "qref" else "ref") + ":" +
      String.format("%08x", p.sig: Integer) + ":" +
      p.path.toString()
    },
    s => {
      val Array(prefix, hex, path) = s.split(":", 3)
      PathRef(
        Path(path),
        prefix match{ case "qref" => true case "ref" => false},
        // Parsing to a long and casting to an int is the only way to make
        // round-trip handling of negative numbers work =(
        java.lang.Long.parseLong(hex, 16).toInt
      )
    }
  )
}