2.6 KiB · text History b868907
defmodule Mix.Tasks.Gitgud.BackfillFdroidIcons do
  @moduledoc """
  Re-read published APKs so the F-Droid index can show a name and an
  icon for apps that were published before it could.

  Both are read out of the APK at upload and stored with the file, so
  an app published before the adapter could resolve `resources.arsc`
  carries neither — and nothing re-reads an APK afterwards. Its entry
  shows the package id and no icon until it publishes again, which for
  an app that isn't building today is never.

      mix gitgud.backfill_fdroid_icons             # newest APK per app
      mix gitgud.backfill_fdroid_icons --all       # every version
      mix gitgud.backfill_fdroid_icons --dry-run   # say what would change

  The newest APK per app is the default because it is what the index
  reads: `name` and `icon` come from the highest versionCode, and
  re-reading twenty older builds would pull twenty APKs out of storage
  to change nothing anyone can see.

  Starts the Repo alone rather than the whole application, so it can be
  run against a live instance — booting the app would fight the running
  server for its fixed SSH and metrics ports.

  The walk itself is `GitGud.Packages.Formats.Fdroid.backfill_icons/1`,
  shared with `GitGud.Release.backfill_fdroid_icons/1` — a release
  ships no Mix, so a deployed instance needs the other entry point, and
  two copies of this would drift.
  """
  @shortdoc "Fill in F-Droid app names and icons from published APKs"

  use Mix.Task

  alias GitGud.Packages.Formats.Fdroid

  @impl true
  def run(args) do
    {opts, _rest, _invalid} =
      OptionParser.parse(args, strict: [all: :boolean, dry_run: :boolean])

    Mix.Task.run("app.config")
    {:ok, _} = Application.ensure_all_started(:ecto_sql)
    {:ok, _} = Application.ensure_all_started(:postgrex)

    started? =
      case GitGud.Repo.start_link() do
        {:ok, _pid} -> true
        {:error, {:already_started, _pid}} -> false
      end

    {read, written} =
      Fdroid.backfill_icons(Keyword.put(opts, :on_file, &Mix.shell().info(report(&1))))

    Mix.shell().info("\n#{read} APK(s) read, #{written} updated.")

    if started?, do: GitGud.Repo.stop()
  end

  defp report(%{error: reason, filename: filename}),
    do: "#{filename}: could not read — #{inspect(reason)}"

  defp report(%{package: package, filename: filename} = file) do
    icon =
      case file.icon do
        {size, ext} -> "#{size} byte .#{ext} icon"
        :none -> "no icon"
      end

    name = file.label || "no name (falls back to #{package})"

    "#{filename}: #{name}, #{icon}#{if file.written?, do: "", else: " — not written"}"
  end
end