src/Tools/VSCode/src/component_vscodium.scala
author wenzelm
Mon, 08 Sep 2025 21:31:19 +0200
changeset 83113 d66ff11d99ed
parent 83112 60f2bddefd26
child 83114 310f1c728ed2
permissions -rw-r--r--
proper platform_context.execute, where this is meant to run on the native platform;

/*  Title:      Tools/VSCode/src/component_vscodium.scala
    Author:     Makarius

Build the Isabelle system component for VSCodium: cross-compilation for all
platforms.

See also https://github.com/VSCodium/vscodium/blob/master/docs/howto-build.md
*/

package isabelle.vscode


import isabelle._

import java.security.MessageDigest
import java.util.Base64


object Component_VSCodium {
  /* vscode parameters */

  val default_node_version = Nodejs.default_version
  val default_vscodium_version = "1.103.25610"

  val vscodium_repository = "https://github.com/VSCodium/vscodium.git"
  val vscodium_download = "https://github.com/VSCodium/vscodium/releases/download"

  def vscode_arch(platform: Isabelle_Platform): String =
    if (platform.is_arm) "arm64" else "x64"

  def vscode_os_name(platform: Isabelle_Platform): String =
    if (platform.is_windows) "windows"
    else if (platform.is_macos) "osx"
    else "linux"

  def vscode_platform_name(platform: Isabelle_Platform): String =
    if (platform.is_windows) "win32"
    else if (platform.is_macos) "darwin"
    else "linux"

  def vscode_platform(platform: Isabelle_Platform): String =
    vscode_platform_name(platform) + "-" + vscode_arch(platform)

  private val resources = Path.explode("resources")

  private def read_patch(name: String): String =
    File.read(Path.explode("$ISABELLE_VSCODE_HOME/patches") + Path.basic(name).patch)


  /* build environment */

  val build_env: List[String] =
    List(
      "VSCODE_QUALITY=stable",
      "VSCODE_LATEST=no",
      "CI_BUILD=no",
      "SKIP_ASSETS=yes",
      "SHOULD_BUILD=yes",
      "SHOULD_BUILD_REH=no",
      "SHOULD_BUILD_REH_WEB=no")

  def build_upstream_env(dir: Path): List[String] = {
    val str = File.read(dir + Path.explode("upstream/stable.json"))
    val json = JSON.parse(str)
    (for {
      tag <- JSON.string(json, "tag")
      commit <- JSON.string(json, "commit")
    } yield List("MS_TAG=" + tag, "MS_COMMIT=" + commit))
      .getOrElse(error("Malformed upstream information:\n" + str))
  }


  /* Isabelle symbols (static subset only) */

  lazy val symbols: Symbol.Symbols =
    Symbol.Symbols.make(File.read(Path.explode("~~/etc/symbols")))

  def make_symbols(): File.Content = {
    val symbols_js =
      JSON.Format.pretty_print(
        for (entry <- symbols.entries) yield
          JSON.Object(
            "symbol" -> entry.symbol,
            "name" -> entry.name,
            "abbrevs" -> entry.abbrevs) ++
          JSON.optional("code", entry.code))

    File.content(Path.explode("symbols.json"), symbols_js)
  }

  def make_isabelle_encoding(header: String): File.Content = {
    val symbols_js =
      JSON.Format.pretty_print(
        for (entry <- symbols.entries; code <- entry.code)
          yield JSON.Object("symbol" -> entry.symbol, "code" -> code))

    val path = Path.explode("isabelle_encoding.ts")
    val body =
      File.read(Path.explode("$ISABELLE_VSCODE_HOME/patches") + path)
        .replace("[/*symbols*/]", symbols_js)
    File.content(path, header + "\n" + body)
  }


  /* platform-specific build context */

  object Build_Context {
    def make(
      platform_context: Isabelle_Platform.Context,
      node_version: String = default_node_version,
      vscodium_version: String = default_vscodium_version
    ): Build_Context = {
      val platform = platform_context.isabelle_platform
      val env1 =
        List(
          "OS_NAME=" + vscode_os_name(platform),
          "VSCODE_ARCH=" + vscode_arch(platform))
      val env2 =
        if (platform.is_windows) {
          List(
            "SHOULD_BUILD_ZIP=no",
            "SHOULD_BUILD_EXE_SYS=no",
            "SHOULD_BUILD_EXE_USR=no",
            "SHOULD_BUILD_MSI=no",
            "SHOULD_BUILD_MSI_NOUP=no")
        }
        else if (platform.is_linux) List("SKIP_LINUX_PACKAGES=True")
        else Nil
      Build_Context(platform_context, node_version, vscodium_version, env1 ::: env2)
    }
  }

  sealed case class Build_Context(
    platform_context: Isabelle_Platform.Context,
    node_version: String,
    vscodium_version: String,
    env: List[String]
  ) {
    def platform: Isabelle_Platform = platform_context.isabelle_platform
    def progress: Progress = platform_context.progress

    def node_setup(base_dir: Path): Nodejs.Directory =
      Nodejs.setup(base_dir, platform = platform, version = node_version,
        packages = List("yarn"), progress = progress)

    def download_ext: String = if (platform.is_linux) "tar.gz" else "zip"

    def download_name: String =
      "VSCodium-" + vscode_platform(platform) + "-" + vscodium_version + "." + download_ext

    def download(dir: Path): Unit = {
      Isabelle_System.with_tmp_file("download", ext = download_ext) { download_file =>
        progress.echo("Getting VSCodium release ...")
        Isabelle_System.download_file(vscodium_download + "/" + vscodium_version + "/" + download_name,
          download_file)
        Isabelle_System.extract(download_file, dir)
      }
    }

    def get_vscodium_repository(build_dir: Path): Unit = {
      progress.echo("Getting VSCodium repository ...")
      Isabelle_System.git_clone(vscodium_repository, build_dir, checkout = vscodium_version)

      progress.echo("Getting VSCode repository ...")
      platform_context.execute(build_dir, environment(build_dir) + "\n" + "./get_repo.sh").check
    }

    def platform_name: String = platform.ISABELLE_PLATFORM(windows = true, apple = true)
    def platform_dir(dir: Path): Path = dir + Path.explode(platform_name)

    def build_dir(dir: Path): Path = dir + Path.basic("VSCode-" + vscode_platform(platform))

    def environment(dir: Path): String =
      Bash.exports((build_env ::: build_upstream_env(dir) ::: env):_*)

    def patch_sources(base_dir: Path): String = {
      val dir = base_dir + Path.explode("vscode")
      Isabelle_System.with_copy_dir(dir, dir.orig) {
        // isabelle_encoding.ts
        {
          val common_dir = dir + Path.explode("src/vs/workbench/services/textfile/common")
          val header =
            split_lines(File.read(common_dir + Path.explode("encoding.ts")))
              .takeWhile(_.trim.nonEmpty)
          make_isabelle_encoding(cat_lines(header)).write(common_dir)
        }

        // explicit patches
        for (name <- Seq("cli", "isabelle_encoding", "no_ocaml_icons")) {
          Isabelle_System.apply_patch(dir, read_patch(name), progress = progress)
        }

        Isabelle_System.make_patch(base_dir, dir.base.orig, dir.base)
      }
    }

    def patch_resources(base_dir: Path): String = {
      val dir = base_dir + resources
      val patch =
        Isabelle_System.with_copy_dir(dir, dir.orig) {
          val fonts_dir = dir + Path.explode("app/out/vs/base/browser/ui/fonts")
          HTML.init_fonts(fonts_dir.dir)
          make_symbols().write(fonts_dir)

          val workbench_css = dir + Path.explode("app/out/vs/workbench/workbench.desktop.main.css")
          val checksum1 = file_checksum(workbench_css)
          File.append(workbench_css, "\n\n" + HTML.fonts_css_dir(prefix = "../base/browser/ui"))
          val checksum2 = file_checksum(workbench_css)

          val file_name = workbench_css.file_name
          File.change_lines(dir + Path.explode("app/product.json")) { _.map(line =>
            if (line.containsSlice(file_name) && line.contains(checksum1)) {
              line.replace(checksum1, checksum2)
            }
            else line)
          }

          Isabelle_System.make_patch(dir.dir, dir.orig.base, dir.base)
        }

      val app_dir = dir + Path.explode("app")
      val vscodium_app_dir = dir + Path.explode("vscodium")
      Isabelle_System.move_file(app_dir, vscodium_app_dir)

      Isabelle_System.make_directory(app_dir)
      if ((vscodium_app_dir + resources).is_dir) {
        Isabelle_System.copy_dir(vscodium_app_dir + resources, app_dir)
      }

      patch
    }

    def setup_electron(dir: Path): Unit = {
      val electron = Path.explode("electron")
      if (platform.is_linux) {
        Isabelle_System.move_file(dir + Path.explode("codium"), dir + electron)
      }
      else if (platform.is_windows) {
        Isabelle_System.move_file(dir + Path.explode("VSCodium.exe"), dir + electron.exe)
        Isabelle_System.move_file(
          dir + Path.explode("VSCodium.VisualElementsManifest.xml"),
          dir + Path.explode("electron.VisualElementsManifest.xml"))
      }
    }

    def setup_executables(dir: Path): Unit = {
      Isabelle_System.rm_tree(dir + Path.explode("bin"))

      if (platform.is_windows) {
        val files =
          File.find_files(dir.file, pred = { file =>
            val name = file.getName
            File.is_dll(name) || File.is_exe(name) || File.is_node(name)
          })
        files.foreach(file => File.set_executable(File.path(file)))
        Isabelle_System.bash("chmod -R o-w " + File.bash_path(dir)).check
      }
    }
  }


  // see https://github.com/microsoft/vscode/blob/main/build/gulpfile.vscode.js
  // function computeChecksum(filename)
  private def file_checksum(path: Path): String = {
    val digest = MessageDigest.getInstance("SHA-256")
    digest.update(Bytes.read(path).make_array)
    Bytes(Base64.getEncoder.encode(digest.digest()))
      .text.replaceAll("=", "")
  }


  /* original repository clones and patches */

  def vscodium_patch(build_context: Build_Context): String = {
    val platform_context = build_context.platform_context
    val progress = build_context.progress

    Isabelle_System.with_tmp_dir("build") { build_dir =>
      build_context.get_vscodium_repository(build_dir)
      val vscode_dir = build_dir + Path.explode("vscode")

      val node_dir = build_context.node_setup(build_dir)

      progress.echo("Preparing VSCode ...")
      Isabelle_System.with_copy_dir(vscode_dir, vscode_dir.orig) {
        platform_context.execute(build_dir,
          "set -e",
          build_context.environment(build_dir),
          node_dir.path_setup,
          "./prepare_vscode.sh",
          // enforce binary diff of code.xpm
          "cp vscode/resources/linux/code.png vscode/resources/linux/rpm/code.xpm").check
        Isabelle_System.make_patch(build_dir, vscode_dir.orig.base, vscode_dir.base,
          diff_options = "--exclude=.git --exclude=node_modules")
      }
    }
  }


  /* build vscodium */

  def component_vscodium(
    target_dir: Path = Path.current,
    node_version: String = default_node_version,
    vscodium_version: String = default_vscodium_version,
    platform_context: Isabelle_Platform.Context = Isabelle_Platform.Context(),
  ): Unit = {
    val platform = platform_context.isabelle_platform
    val progress = platform_context.progress

    val build_context =
      Build_Context.make(platform_context,
        node_version = node_version,
        vscodium_version = vscodium_version)

    Isabelle_System.require_command("git")
    Isabelle_System.require_command("jq")
    Isabelle_System.require_command("rustup")


    /* component */

    val component_name = "vscodium-" + vscodium_version
    val component_dir =
      Components.Directory(target_dir + Path.explode(component_name)).create(progress = progress)


    /* patches */

    progress.echo("\n* Building patches:")

    val patches_dir = Isabelle_System.new_directory(component_dir.path + Path.explode("patches"))

    def write_patch(name: String, patch: String): Unit =
      File.write(patches_dir + Path.explode(name).patch, patch)

    write_patch("01-vscodium", vscodium_patch(build_context))


    /* build */

    Isabelle_System.with_tmp_dir("build") { build_dir =>
      progress.echo("\n* Building VSCodium for " + build_context.platform_name + ":")

      build_context.get_vscodium_repository(build_dir)
      Isabelle_System.apply_patch(build_dir, read_patch("vscodium"), progress = progress)

      val sources_patch = build_context.patch_sources(build_dir)
      write_patch("02-isabelle_sources", sources_patch)

      val node_dir = build_context.node_setup(build_dir)

      progress.echo("Building VSCodium ...")
      val environment = build_context.environment(build_dir)
      progress.echo(environment, verbose = true)
      platform_context.execute(
        build_dir, node_dir.path_setup + "\n" + environment + "./build.sh").check

      Isabelle_System.copy_file(build_dir + Path.explode("LICENSE"), component_dir.path)

      val platform_dir = build_context.platform_dir(component_dir.path)
      Isabelle_System.copy_dir(build_context.build_dir(build_dir), platform_dir)
      if (platform.is_macos) {
        Isabelle_System.symlink(Path.explode("VSCodium.app/Contents/Resources"),
          platform_dir + resources)
      }
      build_context.setup_electron(platform_dir)

      val resources_patch = build_context.patch_resources(platform_dir)
      write_patch("03-isabelle_resources", resources_patch)

      val electron_resources =
        Path.explode("vscode/node_modules/electron/dist") +
          (if (platform.is_macos) Path.explode("Electron.app/Contents/Resources") else resources)
      Isabelle_System.copy_file(
        build_dir + electron_resources + Path.explode("default_app.asar"),
        platform_dir + resources)

      build_context.setup_executables(platform_dir)
    }

    Isabelle_System.bash("gzip *.patch", cwd = patches_dir).check


    /* settings */

    component_dir.write_settings("""
ISABELLE_VSCODIUM_HOME="$COMPONENT/${ISABELLE_WINDOWS_PLATFORM64:-${ISABELLE_APPLE_PLATFORM64:-$ISABELLE_PLATFORM64}}"

case "$ISABELLE_PLATFORM_FAMILY" in
  "macos"*)
    ISABELLE_VSCODIUM_ELECTRON="$ISABELLE_VSCODIUM_HOME/VSCodium.app/Contents/MacOS/Electron"
    ISABELLE_VSCODIUM_RESOURCES="$ISABELLE_VSCODIUM_HOME/VSCodium.app/Contents/Resources"
    ;;
  *)
    ISABELLE_VSCODIUM_ELECTRON="$ISABELLE_VSCODIUM_HOME/electron"
    ISABELLE_VSCODIUM_RESOURCES="$ISABELLE_VSCODIUM_HOME/resources"
    ;;
esac
""")


    /* README */

    File.write(component_dir.README,
      "This is VSCodium " + vscodium_version + " from " + vscodium_repository +
"""

It has been built from sources using "isabelle component_vscodium". This applies
a few changes required for Isabelle/VSCode, see "patches" directory for a
formal record.


        Makarius
        """ + Date.Format.date(Date.now()) + "\n")
  }


  /* Isabelle tool wrappers */

  val isabelle_tool1 =
    Isabelle_Tool("component_vscodium", "build component for VSCodium",
      Scala_Project.here,
      { args =>
        var target_dir = Path.current
        var mingw = MinGW.none
        var node_version = default_node_version
        var vscodium_version = default_vscodium_version
        var verbose = false

        val getopts = Getopts("""
Usage: component_vscodium [OPTIONS]

  Options are:
    -D DIR       target directory (default ".")
    -M DIR       msys/mingw root specification for Windows
    -N VERSION   Node.js version (default: """" + default_node_version + """")
    -V VERSION   VSCodium version (default: """" + default_vscodium_version + """")
    -v           verbose

  Build VSCodium from sources and turn it into an Isabelle component.
""",
          "D:" -> (arg => target_dir = Path.explode(arg)),
          "M:" -> (arg => mingw = MinGW(Path.explode(arg))),
          "N:" -> (arg => node_version = arg),
          "V:" -> (arg => vscodium_version = arg),
          "v" -> (_ => verbose = true))

        val more_args = getopts(args)
        if (more_args.nonEmpty) getopts.usage()

        val progress = new Console_Progress(verbose = verbose)
        val platform_context = Isabelle_Platform.Context(mingw = mingw, progress = progress)

        component_vscodium(target_dir = target_dir, node_version = node_version,
          vscodium_version = vscodium_version, platform_context = platform_context)
      })

  val isabelle_tool2 =
    Isabelle_Tool("vscode_patch", "patch VSCode source tree",
      Scala_Project.here,
      { args =>
        var base_dir = Path.current
        var mingw = MinGW.none
        var verbose = false

        val getopts = Getopts("""
Usage: vscode_patch [OPTIONS]

  Options are:
    -D DIR       base directory (default ".")
    -M DIR       msys/mingw root specification for Windows
    -v           verbose

  Patch original VSCode source tree for use with Isabelle/VSCode.
""",
          "D:" -> (arg => base_dir = Path.explode(arg)),
          "M:" -> (arg => mingw = MinGW(Path.explode(arg))),
          "v" -> (_ => verbose = true))

        val more_args = getopts(args)
        if (more_args.nonEmpty) getopts.usage()

        val progress = new Console_Progress(verbose = verbose)
        val platform_context = Isabelle_Platform.Context(mingw = mingw, progress = progress)

        Build_Context.make(platform_context).patch_sources(base_dir)
      })
}