SharpIco:純粋なC#で依存関係ゼロの.icoアイコン生成器、.NET9とAOTコンパイル対応

SharpIco:純粋なC#で依存関係ゼロの.icoアイコン生成器、.NET9とAOTコンパイル対応

ネットで見つかるICO作成ツールの多くはファビコン向けで、他は重すぎるか有料だったため、私は再びオープンソースツールに目を向けました

最終更新 2025/05/27 20:27
程序设计实验室
読了目安 14 分
カテゴリ
.NET
テーマ
C# AOT
タグ
.NET C# オープンソース AOT ICO

はじめに

最近、今年リリース予定の2つのデスクトップアプリケーションの改良を進めています:動画編集ツール ClipifyAI 記事作成ツール StarBlogPublisher です。

UI自体はほぼ完成しましたが、アイコンはデフォルトのままで、あまりプロフェッショナルに見えません。

そこで、これら2つのアプリケーションに見栄えの良いアイコンを設定することにしました。

昔、VB6.0 の時代に、オープンソースの ICO アイコン作成ツールを使ったことがありますが、現在はもう見つかりません。

ネットで検索して見つかる ICO 作成ツールの多くは、favicon 向けのものです。

その他は、重すぎたり、有料だったりするため、再びオープンソースツールに注目しました。

Node.js ベースで依存関係のない(ほとんどが C++ 実装の画像ライブラリ Magick に依存している)ツールを見つけましたが、残念ながら私の環境では動作しませんでした。

目標

そこで、以前 C# で画像フォーマット変換ツールを開発したことを思い出しました。このツールは .NET 8 の AOT 機能を利用しており、Go 言語のようにクロスプラットフォームな単一実行可能ファイルを開発できます。

そこで、C# を使用してアイコン生成ツールを開発することにしました。このツールは以下の機能を実現します:

  • 純粋な C# 実装で、外部依存関係なし、クロスプラットフォーム、単一実行可能ファイル、AOT
  • PNG 画像を複数の異なるサイズ(16、32、48、64、128、256、512)に分割し、1つの ICO アイコンに合成して、異なる画面サイズでの良好な視覚体験を提供
  • Inspect 機能をサポートし、ICO アイコンを読み取って分析可能
  • 便利な配布方法:dotnet tool、scoop、brew などのツールによるワンクリックインストールをサポート

現在、開発は完了し、nuget と scoop にも公開しました。次に、brew への公開方法を検討します。

プロジェクトページ: https://github.com/star-plan/sharp-ico

実装

SharpIco では、.ico ファイルの生成に ImageMagick やその他の外部画像処理ツールを一切使用せず、純粋な C# コードで仕様に準拠した ICO バイナリ構造を手動で構築します。

この部分の中核クラスは IcoGenerator です。具体的なコードはプロジェクト内にありますので、ここではいくつかの要点を紹介します。

マルチサイズ画像の生成

ImageSharp を使用してマルチサイズ画像を生成します。

var clone = original.Clone(ctx => ctx.Resize(size, size));
clone.SaveAsPng(ms);
  • ImageSharp の Resize と Clone 機能を使用して、元の高解像度 PNG から複数のターゲットサイズ(16x16、32x32、256x256 など)を生成します。
  • PNG 形式でメモリストリームに保存し、後続の .ico 書き込みに使用します。

💡 ICO ファイルは(Vista 以降)PNG 画像の埋め込みをサポートしており、これによりファイルサイズを小さく保ち、透明度の効果を向上させることができます。

ICO ファイルヘッダの手動構築

ICO ファイル形式に従って、ICONDIR と ICONDIRENTRY を手動で構築します。

ICO ファイルのヘッダは次の3つの部分で構成されます:

  • ICONDIR(6バイト):固定構造
  • ICONDIRENTRY × N(各16バイト):埋め込まれた各画像のサイズ、オフセットを記述
  • Image Data × N:実際の画像バイナリデータ
writer.Write((ushort)0); // Reserved
writer.Write((ushort)1); // Type = icon
writer.Write((ushort)images.Count); // Image count
  • 最初に ICO ファイルヘッダ ICONDIR を書き込みます。
  • 次に、各画像の記述情報(幅、高さ、ビット深度、データオフセットなど)をループで書き込みます。
writer.Write((byte)(img.Width == 256 ? 0 : img.Width)); // 256 は 0 で表現
writer.Write((ushort)32); // bits per pixel
writer.Write(image.Length);
writer.Write(offset);

ICO ファイルの幅/高さフィールドが 0 の場合、256 を意味します。 これは ICO 形式の特別な規定です。

ICO ファイル形式では、画像サイズを表す際に幅と高さのフィールドがそれぞれ1バイトで、値の範囲は 0~255 です。これらのフィールドが 0 の場合、仕様では 256 ピクセルを意味します。512×512 や 1024×1024 などの 256 より大きいサイズの場合、ファイルヘッダでは依然として 0(つまり 256)と表示されますが、実際の画像データにはより大きなサイズの画像を含めることができます。

画像の結合

すべての PNG 画像データを結合します。

foreach (var image in images) {
    writer.Write(image);
}
  • すべての記述情報を書き込んだ後、すぐに画像データ本体を書き込みます。
  • オフセットを事前に計算しているため、各画像データがシステムによって正しく認識および読み取られることが保証されます。

拡張

カスタムサイズをサポートします。

public static void GenerateIcon(string sourcePng, string outputIco, int[] sizes)
  • デフォルトでは 16~512 の一般的なサイズをサポートします。
  • パラメータを介してサイズの組み合わせを柔軟に指定できます(例:32/256 のみをパッケージ化)。

Inspect 機能

アイコン生成に加えて、SharpIco にはアイコンコンテンツ分析ツール IcoInspector が組み込まれています。これにより、開発者は .ico ファイルの内部構造を深く理解し、実際に含まれているレイヤー画像のサイズやビット深度情報を検証し、市販のアイコンツールが生成する不適切な .ico ファイルの問題を解決できます。

ここでは、実装の考え方を簡単に紹介します。

ICO ファイルヘッダの読み取り

ICONDIR + ICONDIRENTRY 構造を手動で読み取ります。

ICO ファイルのヘッダ構造は3つの部分で構成されます:

  • ICONDIR(6バイト):アイコンとしてマーク、画像数を記録
  • ICONDIRENTRY × N(各16バイト):各画像のメタ情報(幅、高さ、ビット深度、データオフセットなど)を記録
  • 画像データブロック:実際の PNG または BMP 画像
ushort reserved = reader.ReadUInt16(); // 0でなければならない
ushort type = reader.ReadUInt16();     // 1はアイコンを表す
ushort count = reader.ReadUInt16();    // 画像数

その後、画像エントリを1つずつ読み取り、後続の処理のためにメモリに保存します。

byte width = reader.ReadByte();
byte height = reader.ReadByte();
ushort bitCount = reader.ReadUInt16();
int sizeInBytes = reader.ReadInt32();
int imageOffset = reader.ReadInt32();

そういえば、前述したように、ICO ヘッダのスペースは限られており、8ビットしか格納できません。そのため、width/height フィールドが 0 の場合、仕様に従って 256、または 256 を超えるサイズを意味します。

画像データの抽出と解析

画像データを抽出し、実際の解像度を検証します。

ICO ファイルに記録された幅と高さは必ずしも正確ではありません。特に PNG 形式が埋め込まれている場合です。そのため、ImageSharp を使用して各画像を実際に解析します。

fs.Seek(entry.ImageOffset, SeekOrigin.Begin);
fs.Read(imageData, 0, dataSize);
Image.Load(imageData) → 実際の Width と Height を取得
  • GetImageDimensions() メソッドで PNG かどうかを判断し、ImageSharp で読み込みます。
  • 形式が間違っているか読み取りに失敗した場合、ヘッダで宣言されたサイズにフォールバックします。

これにより、特定の「偽造 ICO」の問題(サイズと画像内容の不一致など)を検出できます。

分析結果の補完と出力

すべてのエントリを読み取り、解析した後、構造化情報として出力します。

ICOファイルを検査中: logo.ico
アイコン数: 7
- 1番目の画像: 16x16, 32bpp, サイズ: 840バイト, オフセット: 118
- 2番目の画像: 32x32, 32bpp, サイズ: 1939バイト, オフセット: 958
- 3番目の画像: 48x48, 32bpp, サイズ: 3375バイト, オフセット: 2897
- 4番目の画像: 64x64, 32bpp, サイズ: 4951バイト, オフセット: 6272
- 5番目の画像: 128x128, 32bpp, サイズ: 13782バイト, オフセット: 11223
- 6番目の画像: 256x256, 32bpp, サイズ: 37823バイト, オフセット: 25005
- 7番目の画像: 512x512, 32bpp, サイズ: 114655バイト, オフセット: 62828
  注意: ファイルヘッダで指定されたサイズは256x256ですが、実際の画像サイズは512x512です

これにより、以下を迅速に確認できます:

  • .ico ファイルに含まれるレイヤー数
  • 各レイヤーの実際のサイズとビット深度
  • フォーマットの問題(サイズの不一致、サイズ異常など)の有無

コマンドラインインターフェースの設計

SharpIco はコードライブラリとしてだけでなく、完全なコマンドラインツールも提供しており、あらゆる開発シナリオで手動使用またはビルドスクリプトへの統合を容易に行えます。

この部分では、.NET のモダンな CLI 構築ライブラリ System.CommandLine を使用して、2つのメインコマンドを実装しています:

  • generate:PNG 画像を ICO アイコンに変換します
  • inspect:ICO ファイルの構造とレイヤー情報をチェックします

コマンドラインの使用法:

sharpico generate -i logo.png -o icon.ico --sizes 16 32 48 256
sharpico inspect icon.ico

🛠️ 生成コマンド generate

このコマンドは、パラメータを介して入力、出力パス、および生成するアイコンのサイズを制御できます:

sharpico generate --input logo.png --output app.ico --sizes 16 32 64 256

パラメータ説明:

パラメータ 省略形 説明
--input -i ソース PNG 画像パス(必須)
--output -o 出力 ICO ファイルパス(必須)
--sizes -s 生成するアイコンサイズのリスト、複数指定可(デフォルトは7つの一般的なサイズ)

複数のサイズを同時に生成できます。内部的に IcoGenerator.GenerateIcon() を呼び出して、マルチレイヤーの .ico ファイルを作成します。

例:

sharpico generate -i logo.png -o icon.ico -s 32 64 256

出力には3つの解像度のレイヤーが含まれます。

🔍 検査コマンド inspect

任意の .ico ファイルの構造チェックと検証を実行できます:

sharpico inspect icon.ico

これにより、アイコン内の各レイヤーの以下が出力されます:

  • 幅と高さ(宣言サイズと実際のサイズ)
  • ビット深度(bpp)
  • データオフセットとサイズ
  • ヘッダと実際の画像サイズの不一致などの潜在的な問題の有無

出力例:

ICOファイルを検査中: logo.ico
アイコン数: 7
- 1番目の画像: 16x16, 32bpp, サイズ: 840バイト, オフセット: 118
- 2番目の画像: 32x32, 32bpp, サイズ: 1939バイト, オフセット: 958
- 3番目の画像: 48x48, 32bpp, サイズ: 3375バイト, オフセット: 2897
- 4番目の画像: 64x64, 32bpp, サイズ: 4951バイト, オフセット: 6272
- 5番目の画像: 128x128, 32bpp, サイズ: 13782バイト, オフセット: 11223
- 6番目の画像: 256x256, 32bpp, サイズ: 37823バイト, オフセット: 25005
- 7番目の画像: 512x512, 32bpp, サイズ: 114655バイト, オフセット: 62828
  注意: ファイルヘッダで指定されたサイズは256x256ですが、実際の画像サイズは512x512です

公開

SharpIco は、ライブラリやソースコードのみで使用できる小さなツールではありません。dotnet tool で全プラットフォームにインストールして実行できAOT コンパイルによるパフォーマンスとサイズの最適化をサポートするプロフェッショナルなアイコンツールです。

今回は、AOT と従来の公開の両方をサポートする必要があるため、プロジェクトファイルにいくつかの設定を行いました。

<Project Sdk="Microsoft.NET.Sdk">

    <PropertyGroup>
        <OutputType>Exe</OutputType>
        <TargetFramework>net9.0</TargetFramework>
        <ImplicitUsings>enable</ImplicitUsings>
        <Nullable>enable</Nullable>
        <!-- AOTコンパイル設定は条件付きプロパティグループに移動 -->
        <InvariantGlobalization>true</InvariantGlobalization>

        <!-- .NET Tool 設定 -->
        <PackAsTool>true</PackAsTool>
        <ToolCommandName>sharpico</ToolCommandName>
        <PackageOutputPath>./nupkg</PackageOutputPath>

        <!-- パッケージ情報 -->
        <PackageId>SharpIco</PackageId>
        <Version>1.0.0</Version>
        <Authors>StarPlan</Authors>
        <Description>SharpIcoは、純粋な C# AOT 実装の軽量アイコン生成ツールで、ICOアイコンファイルの生成と検査を行います。高解像度のPNG画像から、複数のサイズ(16x16 から 512x512)を含む標準的なWindows .icoアイコンファイルをワンクリックで生成でき、カスタムサイズもサポートします。アイコン生成に加えて、SharpIcoにはアイコン構造解析機能が組み込まれており、.icoファイルに含まれるレイヤーとサイズを簡単に検証できます。</Description>
        <PackageTags>icon;ico;png;converter,DealiAxy,cli,tool,dotnet-tool,imagesharp</PackageTags>
        <PackageProjectUrl>https://github.com/star-plan/sharp-ico</PackageProjectUrl>
        <RepositoryUrl>https://github.com/star-plan/sharp-ico</RepositoryUrl>
        <PackageLicenseExpression>MIT</PackageLicenseExpression>
        <PackageReadmeFile>README.md</PackageReadmeFile>
    </PropertyGroup>

    <!-- AOT公開専用設定 -->
    <PropertyGroup Condition="'$(PublishAot)' == 'true'">
        <PublishAot>true</PublishAot>
        <TrimMode>full</TrimMode>
        <InvariantGlobalization>true</InvariantGlobalization>
        <IlcGenerateStackTraceData>false</IlcGenerateStackTraceData>
        <IlcOptimizationPreference>Size</IlcOptimizationPreference>
        <IlcFoldIdenticalMethodBodies>true</IlcFoldIdenticalMethodBodies>
        <JsonSerializerIsReflectionEnabledByDefault>true</JsonSerializerIsReflectionEnabledByDefault>
    </PropertyGroup>

    <ItemGroup>
        <None Include="README.md" Pack="true" PackagePath="\" />
    </ItemGroup>

    <ItemGroup>
        <PackageReference Include="SixLabors.ImageSharp" Version="3.1.8" />
        <PackageReference Include="System.CommandLine" Version="2.0.0-beta4.22272.1" />
    </ItemGroup>

</Project>

いくつかの要点:

CLI ツール(dotnet tool)として定義

<PackAsTool>true</PackAsTool>
<ToolCommandName>sharpico</ToolCommandName>

これにより、SharpIco は他の .NET CLI ツール と同様にグローバルにインストールできるようになります:

dotnet tool install -g SharpIco --add-source ./nupkg

インストール後、次のように実行できます:

sharpico generate -i logo.png -o icon.ico

AOT コンパイル公開のサポート(.NET 9 ネイティブサポート)

<PublishAot>true</PublishAot>
<IlcOptimizationPreference>Size</IlcOptimizationPreference>
<TrimMode>full</TrimMode>

PublishAot=true を設定することで、SharpIco はネイティブ実行可能ファイルにコンパイルできます。

.NET ランタイムが不要で、起動が非常に高速であり、ビルドツールチェーンや統合環境での使用に適しています。

AOT ビルドコマンドの例:

dotnet publish -c Release -r win-x64 /p:PublishAot=true

生成された sharpico.exe は、純粋なネイティブ Windows 実行可能ファイルであり、.NET のインストールは不要です!

Windows 以外にも、linux-x64osx-arm64 などのクロスプラットフォームターゲットに公開できます。

NuGet の自動公開

SharpIco は完全な GitHub Actions CI/CD パイプラインを採用しており、一度タグをプッシュするだけで、全プラットフォームのビルドが自動的に完了します

セマンティックバージョニング形式のタグ(例:v1.0.0)をプッシュするだけで、システムが自動的に以下の処理を実行します:

  1. NuGet ツールパッケージのビルドと公開
  2. Windows / Linux / macOS 向けのネイティブ AOT 実行可能ファイルのコンパイル
  3. すべての成果物を GitHub Release ページに自動アップロード

以前、NuGet の公開に関する記事を書いています。詳細はこちらを参照してください:

この部分は問題ありません。

ただし、今回は少し異なります。以前公開したのはクラスライブラリとプロジェクトテンプレートでしたが、今回は dotnet tool です。

これは、npx スクリプトや pip ツールなどの概念に似ています。

dotnet tool コマンドを使用してインストールおよび呼び出しが可能です。

ただし、この方法では AOT を使用できず、framework dependent 方式での公開のみ可能です。

NuGet の公開は比較的簡単で、dotnet コマンドを使用します。

dotnet pack -c Release

ただし、重要なのは GitHub Actions を使用してビルドと公開を自動化することです。このフローは以前とほぼ同じです。

name: SharpIco 公開
run-name: ${{ github.actor }} が SharpIco を公開中 🚀

on:
  push:
    tags:
      - "v*.*.*"  # より明確なバージョンフォーマットマッチング

# ワークフロー全体の権限設定
permissions:
  contents: write
  id-token: write
  issues: write

jobs:
  # ステップ 1: NuGet パッケージの公開
  publish-nuget:
    runs-on: ubuntu-latest
    
    steps:
      - uses: actions/checkout@v3
        with:
          fetch-depth: 0  # バージョン番号計算のために全履歴を取得
      
      - name: .NET のセットアップ
        uses: actions/setup-dotnet@v3
        with:
          dotnet-version: 9.0.x
      
      - name: NuGet パッケージのキャッシュ
        uses: actions/cache@v3
        with:
          path: ~/.nuget/packages
          key: ${{ runner.os }}-nuget-${{ hashFiles('**/packages.lock.json') }}
          restore-keys: |
            ${{ runner.os }}-nuget-
      
      - name: バージョン番号の抽出
        id: get_version
        shell: bash
        run: echo "VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_OUTPUT
      
      - name: 依存関係の復元
        run: dotnet restore ./SharpIco/SharpIco.csproj
      
      - name: テストの実行
        run: dotnet test --no-restore

      - name: プロジェクトのビルド
        run: dotnet build --no-restore -c Release --nologo ./SharpIco/SharpIco.csproj -p:Version=${{ steps.get_version.outputs.VERSION }}
      
      - name: NuGet パッケージの作成
        run: dotnet pack -c Release ./SharpIco/SharpIco.csproj -p:PackageVersion=${{ steps.get_version.outputs.VERSION }} --no-build --output ./nupkg
        
      - name: NuGet Gallery への公開
        run: dotnet nuget push ./nupkg/*.nupkg --api-key ${{ secrets.NUGET_GALLERY_TOKEN }} --source https://api.nuget.org/v3/index.json --skip-duplicate

GitHub Release への自動公開

これが最も苦労した部分で、10回以上も繰り返しデバッグしてようやく成功しました😂

GitHub Actions のデバッグが非常に不便だったのは言うまでもありません。

最終的に成功した設定をそのまま掲載します。

name: SharpIco 公開
run-name: ${{ github.actor }} が SharpIco を公開中 🚀

on:
  push:
    tags:
      - "v*.*.*"  # より明確なバージョンフォーマットマッチング

# ワークフロー全体の権限設定
permissions:
  contents: write
  id-token: write
  issues: write

jobs:
  # ステップ 2: 各プラットフォームの実行可能ファイルをコンパイル
  build-executables:
    needs: publish-nuget  # NuGet パッケージ公開後に実行
    strategy:
      fail-fast: false
      matrix:
        kind: ['windows', 'linux', 'macOS']
        include:
          - kind: windows
            os: windows-latest
            target: win-x64
            extension: '.zip'
          - kind: linux
            os: ubuntu-latest
            target: linux-x64
            extension: '.tar.gz'
          - kind: macOS
            os: macos-latest
            target: osx-x64
            extension: '.tar.gz'

    runs-on: ${{ matrix.os }}
    
    steps:
      - uses: actions/checkout@v3
        with:
          fetch-depth: 0  # バージョン番号計算のために全履歴を取得
      
      - name: バージョン番号の抽出
        id: get_version
        shell: bash
        run: echo "VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_OUTPUT
      
      - name: .NET のセットアップ
        uses: actions/setup-dotnet@v3
        with:
          dotnet-version: 9.0.x
      
      - name: NuGet パッケージのキャッシュ
        uses: actions/cache@v3
        with:
          path: ~/.nuget/packages
          key: ${{ runner.os }}-nuget-${{ hashFiles('**/packages.lock.json') }}
          restore-keys: |
            ${{ runner.os }}-nuget-
      
      - name: Linux 依存関係のインストール
        if: matrix.kind == 'linux'
        run: |
          sudo apt-get update
          sudo apt-get install -y clang zlib1g-dev libkrb5-dev

      - name: Windows 環境の設定
        if: matrix.kind == 'windows'
        shell: pwsh
        run: |
          Write-Host "Windows コンパイル環境を設定中..."
          # 最新の開発者ツールを確認
          choco install visualstudio2022buildtools -y --no-progress
      
      - name: 依存関係の復元
        run: dotnet restore ./SharpIco/SharpIco.csproj
      
      - name: AOT コンパイル
        run: |
          echo "${{ matrix.kind }} プラットフォーム向けに AOT コンパイル中..."
          dotnet publish ./SharpIco/SharpIco.csproj -c Release -r ${{ matrix.target }} --self-contained true -p:PublishAot=true -p:Version=${{ steps.get_version.outputs.VERSION }} -o ./publish/${{ matrix.kind }}
      
      - name: Windows 実行可能ファイルのパッケージ化
        if: matrix.kind == 'windows'
        run: |
          cd ./publish/${{ matrix.kind }}
          7z a -tzip ../../SharpIco-${{ matrix.kind }}-${{ steps.get_version.outputs.VERSION }}${{ matrix.extension }} *
      
      - name: Linux/macOS 実行可能ファイルのパッケージ化
        if: matrix.kind != 'windows'
        run: |
          cd ./publish/${{ matrix.kind }}
          tar -czvf ../../SharpIco-${{ matrix.kind }}-${{ steps.get_version.outputs.VERSION }}${{ matrix.extension }} *
      
      # ビルド成果物を作業成果物(artifacts)としてアップロード
      - name: ビルド成果物のアップロード
        uses: actions/upload-artifact@v4
        with:
          name: SharpIco-${{ matrix.kind }}-${{ steps.get_version.outputs.VERSION }}
          path: ./SharpIco-${{ matrix.kind }}-${{ steps.get_version.outputs.VERSION }}${{ matrix.extension }}
          retention-days: 1

  # ステップ 3: 全プラットフォームの実行可能ファイルを GitHub Release に一括アップロード
  upload-to-release:
    needs: build-executables
    runs-on: ubuntu-latest
    
    steps:
      - name: バージョン番号の抽出
        id: get_version
        shell: bash
        run: echo "VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_OUTPUT
      
      # 全プラットフォームのビルド成果物をダウンロード
      - name: Windows ビルド成果物のダウンロード
        uses: actions/download-artifact@v4
        with:
          name: SharpIco-windows-${{ steps.get_version.outputs.VERSION }}
          path: ./artifacts
      
      - name: Linux ビルド成果物のダウンロード
        uses: actions/download-artifact@v4
        with:
          name: SharpIco-linux-${{ steps.get_version.outputs.VERSION }}
          path: ./artifacts
      
      - name: macOS ビルド成果物のダウンロード
        uses: actions/download-artifact@v4
        with:
          name: SharpIco-macOS-${{ steps.get_version.outputs.VERSION }}
          path: ./artifacts
      
      # ダウンロードしたファイルを確認
      - name: ダウンロードしたファイルの一覧表示
        run: ls -la ./artifacts
      
      # GitHub Release に一括アップロード
      - name: 全ファイルを GitHub Release にアップロード
        uses: softprops/action-gh-release@v1
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        with:
          files: ./artifacts/*
          tag_name: ${{ github.ref }}
          fail_on_unmatched_files: false
          draft: false
          name: SharpIco バージョン ${{ steps.get_version.outputs.VERSION }}
          generate_release_notes: true 

ワークフロー概要

ワークフロー全体は3つのジョブで構成されています:

フェーズ 説明
publish-nuget プロジェクトをコンパイルし、 .nupkg パッケージを NuGet.org に公開
build-executables 3大プラットフォーム向けに AOT ネイティブ実行可能ファイルをコンパイル
upload-to-release すべての成果物を現在のタグに対応する GitHub Release にアップロード

NuGet の公開部分は比較的簡単なので、ここでは繰り返しません。

3プラットフォーム向け AOT 実行可能ファイルのコンパイル(build-executables)

GitHub Matrix ビルド戦略を採用し、それぞれ以下を対象としています:

  • win-x64(Windows 実行可能ファイル、ZIP パッケージ)
  • linux-x64(Linux ELF 実行可能ファイル、tar.gz パッケージ)
  • osx-x64(macOS 実行可能ファイル、tar.gz パッケージ)
dotnet publish -c Release -r ${{ matrix.target }} --self-contained true -p:PublishAot=true

各プラットフォームで対応する圧縮パッケージを生成し、upload-artifact を使用して中間アップロードを行います。

GitHub Release へのアップロード(upload-to-release)

このフェーズでは、以前にビルドした成果物を自動的にダウンロードし、現在のバージョンの Release ページにパッケージ化します:

uses: softprops/action-gh-release@v1

リリースページには、更新履歴が自動生成されます(generate_release_notes: true)。

最終的な効果は以下の図のようになります:

🔖 v1.0.0
├── SharpIco-windows-v1.0.0.zip
├── SharpIco-linux-v1.0.0.tar.gz
└── SharpIco-macOS-v1.0.0.tar.gz

ハマりポイント

ここで最大のハマりポイントは、当初 actions/upload-artifact@v3 を使用していたところ、常にエラーが発生したことです。その後、Issue を調べたところ、非推奨になっていることがわかりました。

v4 にアップグレードすることで問題は解決しました。

さらに、GitHub Release を公開する際に、常に競合が発生し、「すでに存在する」というエラーが出続けました。

append を設定してもダメでした。

その後、一度中間アップロードを行い、後で一括公開する方式に変更して、ようやく成功しました😂

まとめ

SharpIco は、.NET 9 と AOT コンパイル機能を活用した、純粋な C# によるアイコンツールです。面倒な依存関係を排除し、アイコン生成と検証の手順を簡素化することを目的としています。

ImageSharp による画像処理、BinaryWriter による手動での ICO 形式の構築、System.CommandLine によるコマンドライン体験、そして GitHub Actions による全自動パブリッシュワークフローまで、SharpIco は実用的なツールであると同時に、小規模ながら洗練されたエンジニアリングの探求でもあります。

これは、私が理想とするツール像を体現しています。軽量で、純粋で、統合が容易で、クロスプラットフォーム、そしてすぐに使えること。

  • Python、Node、ImageMagick のインストールは不要
  • AOT コンパイルをサポートし、ネイティブ実行可能ファイルを生成
  • アイコン生成と構造解析の両方の機能を提供
  • 一度のリリースで、全プラットフォーム向けのビルド成果物を自動生成

Star / Fork / Issue / PR をお待ちしています。ぜひあなたのビルドシステムに統合してください。

📦 GitHub プロジェクトアドレス: 👉 https://github.com/star-plan/sharp-ico

さらに探索

関連読書

その他の記事
同じカテゴリ / 同じテーマ 2026/02/07

AOTの使用経験のまとめ

プロジェクト作成当初から、新機能を追加したり新しい構文を使用したりした場合には、すぐにAOT公開テストを実施するという良い習慣を身につけるべきです。

続きを読む
同じカテゴリ / 同じテーマ 2023/08/29

.NET 8.0 AOT DebugView

DebugView は、ローカルシステムまたはTCP/IP経由でアクセス可能なネットワーク上の任意のコンピュータのデバッグ出力を監視できるアプリケーションです。

続きを読む