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

macOS向け自動ビルドの際に.dylibファイルの問題を検出・修正するスクリプトを追加 #191

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

capture_output=Trueでもできるかもです。
(修正はしてもしなくてもいいと思います)

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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ここにあるコードはおそらくずっと変わらないと思います。
将来みたい人がどういう目的かぱっとわかるように、このファイルが行うことを一言だけ一番上にdocstringを書いて頂けると!

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)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ここから上の処理は結構似ているので、1つのファイルにしても良いかもと思いました。
(が、まああまりいじられないコードだとも思うので、このままでも良いと思います)

# 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)