Skip to content

Commit

Permalink
macOS向け自動ビルドの際に.dylibファイルの問題を検出・修正するスクリプトを追加 (VOICEVOX#191)
Browse files Browse the repository at this point in the history
* run build.yml on every push (for test-build)

* macOSビルドの.dylibファイル関連の修正を行うスクリプトを追加

* macOS向け自動ビルド時に.dylibファイル関連の修正スクリプトを実行

* fix typo

* run build.yml on master push

* strオブジェクトへの不要な変換を削除

* add docstring
  • Loading branch information
PickledChair authored and y-chan committed Nov 23, 2021
1 parent c5af440 commit b5db92f
Show file tree
Hide file tree
Showing 5 changed files with 226 additions and 0 deletions.
12 changes: 12 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,18 @@ jobs:
run: |
install_name_tool -add_rpath @executable_path/. build/run.dist/run
# NOTE: This task should ideally be done by Nuitka in the `Build run.py` step.
# Please remove this step when you have solved the problem with Nuitka.
- name: Copy the missing .dylib files into the distribution
run: |
python build_util/macos/copy_missing_dylibs.py build/run.dist/
# NOTE: This task should ideally be done by Nuitka in the `Build run.py` step.
# Please remove this step when you have solved the problem with Nuitka.
- name: Fix the rpaths of the .dylib files in the distribution
run: |
python build_util/macos/fix_rpaths.py build/run.dist/
# FIXME: versioned name may be useful; but
# actions/download-artifact and dawidd6/download-artifact do not support
# wildcard / forward-matching yet.
Expand Down
Empty file.
93 changes: 93 additions & 0 deletions build_util/macos/build_util_macos/shlib_tools.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
"""
macOSにおいて共有ライブラリを操作するためのツールをまとめたモジュール
"""

import subprocess
from pathlib import Path
from typing import List


def get_dylib_paths(base_path: Path) -> List[Path]:
"""base_path以下の全てのサブディレクトリにあるdylibファイルのリストを返す"""
return list(base_path.glob("**/*.dylib"))


def get_rpaths(shared_lib_path: Path) -> List[Path]:
"""引数で指定された共有ライブラリのrpathのリストを返す"""
proc = subprocess.run(["otool", "-L", str(shared_lib_path)], stdout=subprocess.PIPE)
output = proc.stdout.decode("utf-8")
paths = [
Path(line.lstrip().split(" ", maxsplit=1)[0])
for line in output.splitlines()[1:]
]
# 得られたパスのリストのうち、共有ライブラリ自体とライブラリ名が同じものは
# rpath ではなく install ID というものなので除外
return [
path
for path in paths
if path.name.split(".")[0] != shared_lib_path.name.split(".")[0]
]


def is_distributable_rpath(rpath: Path) -> bool:
"""開発環境にインストールされたパッケージに依存しないrpathかどうか"""
# 以下のプレフィックスで始まるrpathは配布に際して問題がない
# - プレースホルダ。実行時に自動で解決される
# - @executable_path/
# - @loader_path/
# - @rpath/
# - システム標準のライブラリがあるディレクトリ
# - /usr/lib/
# - /System/Library/Frameworks/
# - /System/Library/PrivateFrameworks/
DISTRIBUTABLE_PREFIXES = [
"@executable_path/",
"@loader_path/",
"@rpath/",
"/usr/lib/",
"/System/Library/Frameworks/",
"/System/Library/PrivateFrameworks/",
]
result = False

for prefix in DISTRIBUTABLE_PREFIXES:
if str(rpath).startswith(prefix):
result = True
break
else:
continue

return result


def change_rpath(old_rpath: Path, new_rpath: Path, dylib_path: Path, base_path: Path):
"""dylib_pathで指定されたdylibのrpathを、old_rpathから、new_rpath(base_pathからの相対パスに変換したもの)に変更する"""
relative_new_rpath = new_rpath.relative_to(base_path)
subprocess.run(
[
"install_name_tool",
"-change",
old_rpath,
"@rpath/" + str(relative_new_rpath),
dylib_path,
]
)


class SharedLib:
"""共有ライブラリの情報"""

__path: Path
__rpaths: List[Path]

def __init__(self, shared_lib_path: Path):
self.__path = shared_lib_path
self.__rpaths = get_rpaths(shared_lib_path)

@property
def path(self) -> Path:
return self.__path

def get_non_distributable_rpaths(self) -> List[Path]:
"""rpathのうち、開発環境に依存しているもののリスト"""
return [rpath for rpath in self.__rpaths if not is_distributable_rpath(rpath)]
54 changes: 54 additions & 0 deletions build_util/macos/copy_missing_dylibs.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
"""
配布物内の.dylibファイルの不足を解消するためのスクリプト
引数で指定したbase_directory以下にある.dylibファイルのrpathをチェックし、
rpathの指す.dylibファイルがbase_directory以下に存在しなかった場合、
rpathの指している場所からその.dylibファイルをbase_directory直下へとコピーする。
"""

import argparse
import shutil
import sys
from pathlib import Path
from typing import List, Set

from build_util_macos.shlib_tools import SharedLib, get_dylib_paths

parser = argparse.ArgumentParser()
parser.add_argument(
"base_directory", help="copy the missing dylibs under base_directory", type=str
)
args = parser.parse_args()
base_dir_path = Path(args.base_directory)

if not (base_dir_path.exists() and base_dir_path.is_dir()):
print("could not find the directory:", str(base_dir_path), file=sys.stderr)
exit(1)

# base_dir_path以下の全てのサブディレクトリを探索して得たdylibのリスト
dylib_paths: List[Path] = get_dylib_paths(base_dir_path)
# 全てのdylibのファイル名のリスト
dylib_names: List[str] = [path.name for path in dylib_paths]

# 開発環境に依存したrpathを持つdylibのリスト
non_distributable_dylibs: List[SharedLib] = []
for dylib_path in dylib_paths:
lib = SharedLib(dylib_path)
if lib.get_non_distributable_rpaths():
non_distributable_dylibs.append(lib)

# 開発環境に依存したrpathの集合
non_distributable_rpaths: Set[Path] = set()
for dylib in non_distributable_dylibs:
rpaths: Set[Path] = set([rpath for rpath in dylib.get_non_distributable_rpaths()])
non_distributable_rpaths = non_distributable_rpaths.union(rpaths)

# rpathが指しているdylibのうち、base_dir_path以下に存在しないもののリスト
external_dylib_paths: List[Path] = []
for rpath in non_distributable_rpaths:
if not (rpath.name in dylib_names):
external_dylib_paths.append(rpath)

# 不足しているdylibをbase_dir_path直下にコピー
for dylib_path in external_dylib_paths:
shutil.copy(dylib_path, base_dir_path, follow_symlinks=True)
67 changes: 67 additions & 0 deletions build_util/macos/fix_rpaths.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
"""
配布物内の.dylibファイルのrpathをどのようなユーザー環境においても有効になるように修正するスクリプト
引数で指定したbase_directory以下にある.dylibファイルのrpathをチェックし、
開発環境に依存した(配布先の環境に存在することが保証されていない)rpathであった場合、
base_directory以下の.dylibファイルを相対パスで指すように変更する。
(base_directory以下の.dylibファイルに不足がないことを前提とする。)
"""

import argparse
import sys
from pathlib import Path
from typing import List, Set

from build_util_macos.shlib_tools import SharedLib, change_rpath, get_dylib_paths

parser = argparse.ArgumentParser()
parser.add_argument(
"base_directory", help="fix the rpaths of the dylibs under base_directory", type=str
)
args = parser.parse_args()
base_dir_path = Path(args.base_directory)

if not (base_dir_path.exists() and base_dir_path.is_dir()):
print("could not find the directory:", str(base_dir_path), file=sys.stderr)
exit(1)

# base_dir_path以下の全てのサブディレクトリを探索して得たdylibのリスト
internal_dylib_paths: List[Path] = get_dylib_paths(base_dir_path)
# 全てのdylibのファイル名のリスト
internal_dylib_names: List[str] = [path.name for path in internal_dylib_paths]

# 開発環境に依存したrpathを持つdylibのリスト
non_distributable_dylibs: List[SharedLib] = []
for internal_dylib_path in internal_dylib_paths:
lib = SharedLib(internal_dylib_path)
if lib.get_non_distributable_rpaths():
non_distributable_dylibs.append(lib)

# 開発環境に依存したrpathの集合
non_distributable_rpaths: Set[Path] = set()
for dylib in non_distributable_dylibs:
rpaths: Set[Path] = set([rpath for rpath in dylib.get_non_distributable_rpaths()])
non_distributable_rpaths = non_distributable_rpaths.union(rpaths)

# rpathが指しているdylibのうち、base_dir_path以下に存在しないもののリスト
external_dylib_paths: List[Path] = []
for rpath in non_distributable_rpaths:
if not (rpath.name in internal_dylib_names):
external_dylib_paths.append(rpath)

# base_dir_path以下でdylibが不足している場合は、不足しているdylibを表示して終了
if external_dylib_paths:
print(
f"following dylibs not found under base_dir_path ({str(base_dir_path)}):",
file=sys.stderr,
)
for path in external_dylib_paths:
print(f"\t{path.name}", file=sys.stderr)
exit(1)

# 開発環境に依存したrpathを、base_dir_path以下のdylibを指すように変更
for dylib in non_distributable_dylibs:
for rpath in dylib.get_non_distributable_rpaths():
for internal_dylib_path in internal_dylib_paths:
if internal_dylib_path.name == rpath.name:
change_rpath(rpath, internal_dylib_path, dylib.path, base_dir_path)

0 comments on commit b5db92f

Please sign in to comment.