GradleでprocessResources時にシンボリックリンクを作成(Windows, macOS対応)

Spring Bootを使ったJavaプロジェクトで、 bootRun をしてから起動するまで数分かかっていた。

調べてみると、 processResources タスクで src/main/webapp/WEB-INFbuild/build/resources/main/WEB-INF にコピーしていたが、 src/main/webapp/WEB-INF 配下がNodeパッケージとなっており、node_modules配下のファイルコピーで時間がかかっていた。

取り急ぎ、シンボリックリンクを作って対応したのでメモ。mklinkについて調べたのはこのため。

環境

Java v8.262.10, Gradle v6.3。

Windows 10およびmacOSで動作確認済み。

シンボリックリンクの作成

build.gradle に以下を追加。

Windowsの場合、開発者モードを有効化するか管理者権限で mklink を実行しないとシンボリックリンクが作れないため、PowerShell経由で管理者権限で起動したコマンドプロンプトで、 mklink を実行している。

import java.nio.file.Files

final def isWindows = System.getProperty('os.name')
    .toLowerCase()
    .contains('windows')
final def srcDir = 'src/main/resources/WEB-INF'

task createWebInfSymbolicLink(type: Exec) {
  // Windowsの場合、スラッシュをバックスラッシュに置換
  // FilenameUtils#separatorsToSystem(String) などでもいい
  def normalizePath = { isWindows ? it.replaceAll('/', '\\\\') : it }
  def destDir = "${projectDir}/build/resources/main/WEB-INF"
  def destDirFile = destDir as File
  def normalizedSrcDir = normalizePath("../../../${srcDir}")

  if (destDirFile.directory) {
    def destDirPath = destDirFile.toPath()

    // シンボリックリンクかを判定
    if (Files.isSymbolicLink(destDirPath)) {
      // srcへのシンボリックリンクがあれば作業不要。
      // ExecタスクではExecSpecを返す必要があるため、適当にechoする。
      def destLink = Files.readSymbolicLink(destDirPath)
      if (normalizedSrcDir == destLink.toString()) {
        def executer = isWindows ? ['cmd', '/c'] : []
        return commandLine(executer + ['echo', 'exists link'])
      }

      // シンボリックリンクでも、パスが違っていれば削除
      destDirFile.delete()
    } else {
      // 実ファイルであればディレクトリを削除
      destDirFile.deleteDir()
    }
  }

  // Windows以外であれば、lnでシンボリックリンクを作成
  if (!isWindows) {
    return commandLine('ln', '-s', "${normalizedSrcDir}", "${destDir}")
  }

  // Windowsの場合、コマンドプロンプトからPowerShellを起動し、
  // さらにPowerShellから管理者権限のコマンドプロンプトでmklinkを実行。
  // 直接PowerShellを起動しても大丈夫な気がするが、
  // やったことないのでとりあえずコマンドプロンプト経由で。
  def arguments = [
      '/c', 'mklink', '/d', normalizePath(destDir), normalizedSrcDir
  ]
  commandLine 'cmd', '/c', 'powershell',
      '-Command', '"Start-Process -Verb Runas"',
      '-FilePath', '"cmd"',
      '-ArgumentList', arguments.collect { /"${it}"/ }.join(',')
}

// processResources の後に実行するよう設定
processResources.finalizedBy createWebInfSymbolicLink

振り返り

mklinkシンボリックリンクを作ろうとすると、やはり権限が必要になるのがネック。

今回の方法では管理者権限が必要になるので、 gradle clean などを実行した後だとUACのポップアップが出てしまう。

ディレクトリジャンクションを使って、管理者権限なしで実行できそうなので、そのうち改良したい。