Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ビルドの自動化 #219

Closed
Hiroshiba opened this issue Sep 11, 2021 · 6 comments · Fixed by #264
Closed

ビルドの自動化 #219

Hiroshiba opened this issue Sep 11, 2021 · 6 comments · Fixed by #264

Comments

@Hiroshiba
Copy link
Member

内容

VOICEVOXのコアライブラリが公開されました。 https://github.com/Hiroshiba/voicevox_core
これを用いれば、ビルドを自動化できるはずです。
(ちなみにエンジンのビルドは数十分かかります)

このタスクは非常に長くなると思われます。
ちょっとでも取り掛かった方がいたら、ぜひコメントで知見ややり方を共有して頂けると・・・!

ref https://github.com/Hiroshiba/voicevox_engine/issues/85

@aoirint
Copy link
Member

aoirint commented Sep 24, 2021

#266 で言及した内容です)

#264 で、ひとまずWindows版の自動ビルドを進めようと思っています。

CIの容量制限が1つの課題になっています。

GitHub Actionsの容量制限は14GBで、
VOICEVOX ENGINEのWindows NVIDIA GPU用実行バイナリは、展開時で5.5GBあります。

いまのところ、Electronビルドを実行すると、
VOICEVOX ENGINEが、コピー元、Electronビルド成果物のwin-unpacked、分割前7z、分割後7zと
多数複製されるようになっています。
これに加えてNode.js・Electronビルド環境も含まれ、容量が足りないという状況です。

CIの実装を進めやすくするため、 #266 を作成しましたが、
VOICEVOX ENGINEが存在しなくてもビルドはできるため、
CIでは、Electronビルド時にはVOICEVOX ENGINEを同梱しないようにした上で、あとで結合するような
ことができないかと思っていて、試そうと思っています。

これができれば、ArtifactにElectronのビルド成果物のみを一度アップロードしてから、容量のリセットされた別JobでVOICEVOX ENGINEを結合するような実装にすることができるように思います。
ただ、NSIS側で結合後(インストール時)の7zのハッシュチェックをしていたらできなさそうだな、と思っています。

また、この方法をとる場合、 build/splitNsisArchive.js をCI側で再実装する必要がありそうです。

@aoirint
Copy link
Member

aoirint commented Sep 24, 2021

https://github.com/Hiroshiba/voicevox/pull/266#issuecomment-926968233

7z作成後に元のENGINEを削除するワークアラウンド

build/afterNsisWebArtifactBuild.js で、環境変数にもとづいてコピー元のVOICEVOX ENGINEを削除する処理を呼ぶ実装はできそうに思いました。

@aoirint
Copy link
Member

aoirint commented Sep 25, 2021

Electronビルド時にはVOICEVOX ENGINEを同梱しないようにした上で、あとで結合する

NSISインストーラ・7z生成後、後付けでVOICEVOX ENGINEを結合してもインストーラの実行に失敗しました。サイズまたはハッシュチェックされていそうです。

build/afterNsisWebArtifactBuild.js で、環境変数にもとづいてコピー元のVOICEVOX ENGINEを削除する処理を呼ぶ実装

これを試してみましたが、ログをよく見てみたところ、そもそも win-unpacked を作る段階で容量不足になっていたため、効果がありませんでした..

npm run electron:build を実行する直前の空き容量は 5.7GB です。

VOICEVOX ENGINEのダウンロードキャッシュは しないようにしています。

流れ: win-unpackedの作成→nsis-web/*.7zの作成→7zファイルの分割

@aoirint
Copy link
Member

aoirint commented Sep 25, 2021

electron-builder--dir オプションと --prepackaged オプションを使って、Jobを分割できそうでした。

試した感じでは、以下のような流れを作ることができそうです。

  • build --dirdist_electron以下にENGINEを含まないwin-unpackedlinux-unpackedを生成
  • *-unpacked をArtifactに保存、Job終了
  • 別Jobで *-unpacked Artifactをpath/to/*-unpacked ディレクトリに展開
  • ENGINEのバイナリを path/to/*-unpacked ディレクトリに追加
  • build --prepackaged "path/to/*-unpacked" でNSISインストーラやAppImageを生成

実験中のWorkflow

関連

@Hiroshiba
Copy link
Member Author

Hiroshiba commented Sep 27, 2021

ライセンス情報取得の自動化に役に立つかもと考えて、VOICEVOX製品版で個人的に利用している雑なコードがあるのでメモとして残します。

Pythonコード
import json
import subprocess
import urllib.request
from dataclasses import asdict, dataclass
from pathlib import Path
from typing import List, Optional


@dataclass
class License:
    name: str
    version: Optional[str]
    license: Optional[str]
    text: str


licenses: List[License] = []

# openjtalk
licenses.append(
    License(
        name="Open JTalk",
        version="1.11",
        license="Modified BSD license",
        text=Path("/path/to/open_jtalk/COPYING").read_text(),
    )
)
licenses.append(
    License(
        name="MeCab",
        version=None,
        license="Modified BSD license",
        text=Path("/path/to/open_jtalk/mecab/COPYING").read_text(),
    )
)
licenses.append(
    License(
        name="NAIST Japanese Dictionary",
        version=None,
        license="Modified BSD license",
        text=Path(
            "/path/to/open_jtalk/mecab-naist-jdic/COPYING"
        ).read_text(),
    )
)
with urllib.request.urlopen(
    "https://raw.githubusercontent.com/r9y9/pyopenjtalk/master/pyopenjtalk/htsvoice/LICENSE_mei_normal.htsvoice"
) as res:
    licenses.append(
        License(
            name='HTS Voice "Mei"',
            version=None,
            license="Creative Commons Attribution 3.0 license",
            text=res.read().decode(),
        )
    )


# pytorch
with urllib.request.urlopen(
    "https://raw.githubusercontent.com/pytorch/pytorch/master/LICENSE"
) as res:
    licenses.append(
        License(
            name="PyTorch",
            version="1.9.0",
            license="BSD-style license",
            text=res.read().decode(),
        )
    )

# Python
licenses.append(
    License(
        name="Python",
        version="3.7.11",
        license="Python Software Foundation License",
        text=Path("downloaded/Python 3.7.11.txt").read_text(encoding="utf8"),
    )
)

# pip
licenses_json = json.loads(
    subprocess.run(
        "pip-licenses "
        "--from=mixed "
        "--format=json "
        "--with-urls "
        "--with-license-file "
        "--no-license-path "
        "--ignore-packages each-cpp-forwarder",
        shell=True,
        capture_output=True,
        check=True,
    ).stdout.decode()
)
for license_json in licenses_json:
    license = License(
        name=license_json["Name"],
        version=license_json["Version"],
        license=license_json["License"],
        text=license_json["LicenseText"],
    )
    if license.text == "UNKNOWN":
        if license.name.lower() == "core" and license.version == "0.0.0":
            continue
        elif license.name.lower() == "nuitka":
            with urllib.request.urlopen(
                "https://raw.githubusercontent.com/Nuitka/Nuitka/develop/LICENSE.txt"
            ) as res:
                license.text = res.read().decode()
        elif license.name.lower() == "pyopenjtalk":
            with urllib.request.urlopen(
                "https://raw.githubusercontent.com/r9y9/pyopenjtalk/master/LICENSE.md"
            ) as res:
                license.text = res.read().decode()
        elif license.name.lower() == "python-multipart":
            with urllib.request.urlopen(
                "https://raw.githubusercontent.com/andrew-d/python-multipart/master/LICENSE.txt"
            ) as res:
                license.text = res.read().decode()
        elif license.name.lower() == "romkan":
            with urllib.request.urlopen(
                "https://raw.githubusercontent.com/soimort/python-romkan/master/LICENSE"
            ) as res:
                license.text = res.read().decode()
        elif license.name.lower() == "resampy":
            with urllib.request.urlopen(
                "https://raw.githubusercontent.com/bmcfee/resampy/master/LICENSE"
            ) as res:
                license.text = res.read().decode()
        else:
            # ライセンスがpypiに無い
            raise Exception(license.name)
    licenses.append(license)


# npm
npm_custom_path = (
    Path("/patu/to/voicevox").expanduser().joinpath("license-custom.json")
)
npm_custom_path.write_text(
    '{ "name": "", "version": "", "description": "", "licenses": "", "copyright": "",'
    '"licenseFile": "none", "licenseText": "none", "licenseModified": "no" }'
)
licenses_json = json.loads(
    subprocess.run(
        "license-checker "
        "--production "
        "--excludePrivatePackages "
        "--json "
        "--customPath license-custom.json",
        shell=True,
        capture_output=True,
        check=True,
        cwd=Path("/patu/to/voicevox").expanduser(),
    ).stdout.decode()
)
npm_custom_path.unlink()
for license_json in licenses_json.values():
    assert "licenseFile" in license_json
    licenses.append(
        License(
            name=license_json["name"],
            version=license_json["version"],
            license=license_json["licenses"],
            text=license_json["licenseText"],
        )
    )

# cuda
licenses.append(
    License(
        name="CUDA Toolkit",
        version="11.1.1",
        license=None,
        text=Path("downloaded/CUDA Toolkit v11.1.1.txt").read_text(encoding="utf8"),
    )
)
licenses.append(
    License(
        name="cuDNN",
        version="7.6.5",
        license=None,
        text=Path("downloaded/NVIDIA cuDNN.txt").read_text(encoding="utf8"),
    )
)

# dump
json.dump(
    [asdict(license) for license in licenses],
    Path("/patu/to/voicevox")
    .expanduser()
    .joinpath("public/licenses.json")
    .open("w"),
)

ライセンス周りに関してどうすればいいか調査したときのメモも共有します。

  • pythonはpip-licensesが、npmはlicense-checkerが便利
    • pythonのpypiはライセンステキストを配っていないライブラリが結構あるので、それは個別対応
  • 気をつける必要があるライセンスは、GPL・LGPL
    • GPL・LGPLは製品版VOICEVOXの都合上利用不可(LGPLはなんとかなるかも)
  • Python・CUDA・cuDNNは結局何をソフトウェア側で明記すれば良いのかがよくわからない
    • 法務的な判断ができない
    • とりあえず問題が起きないように、ライセンスの文面が書かれているHTMLをテキストとして載せている
    • CUDA Toolkitは対応するバージョンのEnd User License Agreement
    • cuDNNはSoftware License Agreement
    • PythonはLICENSE AGREEMENT

@aoirint
Copy link
Member

aoirint commented Sep 27, 2021

ライブラリなどのライセンス表示について、以下のような実装をするのがいいように思いました。

  • VOICEVOX ENGINE
    • https://github.com/Hiroshiba/voicevox_engine/pull/125
      • ENGINE側のlicenses.json生成のためのPythonコードを追加
        • Open JTalk
        • MeCab
        • NAIST Japanese Dictionary
        • HTS Voice "Mei"
        • PyTorch
        • Python
        • pip-licenses
          • (ライセンスの種類のAssertion)
        • pip-licensesの例外処理(ライセンスがpypiに無い 場合の個別処理)
        • CUDA Toolkit
        • cuDNN
      • CIにライセンスがpypiに無い 場合のチェックを追加
      • 自動ビルド時にENGINE側のlicenses.jsonを生成・同梱
    • APIにライセンス情報を簡単なHTMLもしくはプレーンテキストに整形して返すエンドポイント / を追加
      • エンドポイント /licenses.json を追加
      • APIではなく、整形済みのHTML/プレーンテキストを同梱してもよいが、licenses.jsonはあとで使うためArtifact/Release Assetで保持
  • VOICEVOX
    • VOICEVOX側のlicenses.json生成のためのスクリプトを追加(npm run?)
      • license-checker
      • (Electron)
      • (Chromium)
    • 自動ビルド
      • VOICEVOX側のlicenses.jsonを生成
      • ENGINE側のlicenses.jsonを取得、VOICEVOX側のlicenses.jsonとマージ
      • public/licenses.jsonに書き出し、同梱

Hiroshiba pushed a commit that referenced this issue Sep 29, 2021
* add VOICEVOX_ENGINE_DIR env for electron-builder

* add linux electron-builder config

* add workflow

* pack VOICEVOX ENGINE

* fix workflow

* rm engine archive to free space

* name steps

* show disk space

* update voicevox engine run path for linux

* revert linux diff

* remove original engine after artifact build

* env check; empty string or undefined

* fail-fast false

* upload win unpacked

* stage build

* prepackaged

* fix prepackage

* add disk space disp

* merge nsis-web artifact

* engine-prepackage

* upload to release

* fix dir

* fix merge condition

* rename job; distributable

* fix engine copy

* fix matrix var name

* fix step name

* fix matrix var ref

* add fixme note to rename before upload to asset

* disable cpu build

* enable cpu engine prepackage

* build distributable only on release

* build on push master

* fix commented step name

* build on push main

* revert removeOriginalEngine diff

* temporary use Hiroshiba/voicevox_engine check-2021-09-25

* use env to define voicevox engine repo/ver

* use env directly (env cannot be used in matrix)

* add noengine-prepackage name and path in matrix

* impl upload-distributable-to-release

* generate public/licenses.json

* ci licenses.json

* mkdir

* engine

* Update build.yml

* npm ci

* license: use packageName if name is undefined

* revert packageName substitution

* skip uploading to release

* parentheses

* add comment

* env SKIP_UPLOADING_RELEASE_ASSET

* commonize os matrix

* fix env usage

* add note about VOICEVOX ENGINE cache

* use .node_version for Node Setup in engine prepackaging

* cahce version env

* use shell bash
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging a pull request may close this issue.

2 participants