///|
fn linux_appdir_path(spec : PackageSpec) -> String {
  path_join(spec.output, "." + executable_name(spec.product_name) + ".AppDir")
}

///|
async fn linux_arch() -> String {
  let (code, output) = @process.collect_output_merged("uname", ["-m"]) catch {
    error =>
      raise LinuxPackagingError::ToolFailure(
        stage="detect architecture",
        detail=@debug.render(Repr(error)),
      )
  }
  guard code == 0 else {
    raise LinuxPackagingError::ToolFailure(
      stage="detect architecture",
      detail="uname exited with code " + code.to_string(),
    )
  }
  match (output.text() catch { _ => "" }).trim().to_owned() {
    "x86_64" | "amd64" => "x86_64"
    "aarch64" | "arm64" => "aarch64"
    arch =>
      raise LinuxPackagingError::InvalidConfiguration(
        detail="unsupported AppImage architecture: " + arch,
      )
  }
}

///|
fn linux_desktop_entry(spec : PackageSpec, name : String) -> String {
  let prefix =
    #|[Desktop Entry]
    #|Type=Application
    #|Terminal=false
    #|Categories=Utility;
  prefix +
  "\nName=" +
  spec.product_name +
  "\n" +
  "Exec=" +
  name +
  "\n" +
  "Icon=" +
  name +
  "\n" +
  "X-AppImage-Version=" +
  spec.version +
  "\n"
}

///|
fn linux_apprun(name : String) -> String {
  let prefix =
    #|#!/bin/sh
    #|HERE="$APPDIR"
  prefix + "\nexec \"$HERE/usr/bin/" + name + "\" \"$@\"\n"
}

///|
async fn package_linux(
  spec : PackageSpec,
  customization : PackageCustomization,
  prepare : async (PackageLayout) -> Unit,
) -> Array[String] {
  guard path_is_file(spec.executable) else {
    raise PackagePlanError::MissingExecutable(path=spec.executable)
  }
  let icon = match first_icon(spec, ".png") {
    Some(icon) => icon
    None =>
      raise LinuxPackagingError::InvalidConfiguration(
        detail="the appimage format requires a PNG icon",
      )
  }
  guard path_is_file(icon) else {
    raise PackagePlanError::MissingIcon(path=icon)
  }
  let appdir = linux_appdir_path(spec)
  remove_tree(appdir)
  errdefer (remove_tree(appdir) catch { _ => () })
  let name = executable_name(spec.product_name)
  let bin = path_join(appdir, "usr/bin")
  let resources = path_join(
    appdir,
    customization.linux_resources_path.unwrap_or("usr/share/" + name),
  )
  let libraries = path_join(appdir, "usr/lib")
  for directory in [bin, resources, libraries] {
    ensure_dir(directory)
  }
  let staged_executable = path_join(bin, name)
  copy_file(spec.executable, staged_executable)
  @async_fs.chmod(staged_executable, 0o755)
  copy_payloads(spec, resources, libraries, bin)
  copy_file(icon, path_join(appdir, name + ".png"))
  copy_file(icon, path_join(appdir, ".DirIcon"))
  write_text(
    path_join(appdir, name + ".desktop"),
    linux_desktop_entry(spec, name),
  )
  let apprun = path_join(appdir, "AppRun")
  write_text(apprun, linux_apprun(name))
  @async_fs.chmod(apprun, 0o755)
  prepare(PackageLayout::{
    root: appdir,
    executable: staged_executable,
    resources,
    libraries,
    executables: bin,
  })
  let arch = linux_arch()
  let destination = path_join(
    spec.output,
    name + "-" + spec.version + "-" + arch + ".AppImage",
  )
  let staging = destination + ".staging"
  remove_tree(staging)
  errdefer (remove_tree(staging) catch { _ => () })
  let (code, output) = @process.collect_output_merged(
    "appimagetool",
    ["--no-appstream", appdir, staging],
    extra_env={ "ARCH": arch },
  ) catch {
    error =>
      raise LinuxPackagingError::ToolFailure(
        stage="create AppImage",
        detail=@debug.render(Repr(error)),
      )
  }
  guard code == 0 else {
    raise LinuxPackagingError::ToolFailure(
      stage="create AppImage",
      detail="exit code " +
        code.to_string() +
        ": " +
        (output.text() catch { _ => "non-UTF-8 output" }),
    )
  }
  promote(staging, destination)
  remove_tree(appdir)
  [destination]
}