Skip to content

Commit bbc7ef8

Browse files
authored
Merge pull request #208 from bgilbert/direct
Add tool for combining macOS builds into universal build
2 parents 7c7f2d0 + 1690afa commit bbc7ef8

File tree

4 files changed

+308
-31
lines changed

4 files changed

+308
-31
lines changed

.github/workflows/direct.yml

+26-29
Original file line numberDiff line numberDiff line change
@@ -25,27 +25,25 @@ jobs:
2525
key: direct-cache
2626
path: subprojects/packagecache
2727
- name: Build
28+
id: build
2829
run: |
2930
meson setup build --native-file machines/native-linux-x86_64.ini \
3031
-Dopenslide:werror=true -Dopenslide-java:werror=true
3132
meson compile -C build
32-
DESTDIR=install meson install -C build
33-
mkdir output
34-
cp build/install/{bin/slidetool,lib64/libopenslide.so.1} output/
35-
cd output
36-
for f in libopenslide.so.1 slidetool; do
37-
objcopy --only-keep-debug $f ${f}.debug
38-
objcopy -S --add-gnu-debuglink=${f}.debug $f ${f}.new
39-
mv ${f}.new $f
40-
done
41-
patchelf --set-rpath '$ORIGIN' slidetool
33+
artifact=$(cd build/artifacts && echo openslide-bin-*-linux-x86_64.tar.xz)
34+
mv "build/artifacts/$artifact" .
35+
echo "artifact=$artifact" >> $GITHUB_OUTPUT
4236
- name: Smoke test
43-
run: OPENSLIDE_DEBUG=synthetic output/slidetool prop list ''
37+
run: |
38+
tar xf "${{ steps.build.outputs.artifact }}"
39+
OPENSLIDE_DEBUG=synthetic \
40+
openslide-bin-*-linux-x86_64/bin/slidetool prop list ''
4441
- name: Upload artifact
4542
uses: actions/upload-artifact@v4
4643
with:
47-
name: linux
48-
path: output
44+
name: ${{ steps.build.outputs.artifact }}
45+
path: ${{ steps.build.outputs.artifact }}
46+
compression-level: 0
4947
macos:
5048
name: macOS
5149
runs-on: macos-latest
@@ -68,29 +66,28 @@ jobs:
6866
key: direct-cache
6967
path: subprojects/packagecache
7068
- name: Build
69+
id: build
7170
run: |
71+
export OPENSLIDE_BIN_SUFFIX="$(date +%Y%m%d).local"
72+
version=$(MESON_SOURCE_ROOT=$(pwd) python3 utils/get-version.py)
7273
for arch in x86_64 arm64; do
7374
meson setup $arch --cross-file machines/cross-macos-${arch}.ini \
7475
-Dopenslide:werror=true -Dopenslide-java:werror=true
7576
meson compile -C $arch
76-
DESTDIR=install meson install -C $arch
7777
done
78-
mkdir output
79-
lipo -create {x86_64,arm64}/install/lib/libopenslide.1.dylib \
80-
-output output/libopenslide.1.dylib
81-
lipo -create {x86_64,arm64}/install/bin/slidetool \
82-
-output output/slidetool
83-
cd output
84-
for f in libopenslide.1.dylib slidetool; do
85-
dsymutil $f
86-
strip -u -r $f
87-
done
88-
install_name_tool -change /lib/libopenslide.1.dylib \
89-
'@loader_path/libopenslide.1.dylib' slidetool
78+
artifact="openslide-bin-${version}-macos-arm64-x86_64.tar.xz"
79+
PYTHONPATH=. python3 utils/write-universal-bdist.py \
80+
-o "$artifact" \
81+
*/artifacts/openslide-bin-"${version}"-macos-*.tar.xz
82+
echo "artifact=$artifact" >> $GITHUB_OUTPUT
9083
- name: Smoke test
91-
run: OPENSLIDE_DEBUG=synthetic output/slidetool prop list ''
84+
run: |
85+
tar xf "${{ steps.build.outputs.artifact }}"
86+
OPENSLIDE_DEBUG=synthetic \
87+
openslide-bin-*-macos-arm64-x86_64/bin/slidetool prop list ''
9288
- name: Upload artifact
9389
uses: actions/upload-artifact@v4
9490
with:
95-
name: macos
96-
path: output
91+
name: ${{ steps.build.outputs.artifact }}
92+
path: ${{ steps.build.outputs.artifact }}
93+
compression-level: 0

common/archive.py

+115-2
Original file line numberDiff line numberDiff line change
@@ -20,21 +20,36 @@
2020
from __future__ import annotations
2121

2222
from abc import ABC, abstractmethod
23+
from collections.abc import Iterable, Iterator, Sequence
24+
from contextlib import ExitStack, contextmanager
25+
import copy
2326
from dataclasses import dataclass
27+
from functools import cached_property
28+
from itertools import zip_longest
2429
import os
2530
from pathlib import Path, PurePath
2631
import re
2732
import tarfile
33+
import tempfile
2834
import time
2935
from types import TracebackType
30-
from typing import BinaryIO, Self
36+
from typing import BinaryIO, Self, cast
3137
import zipfile
3238

3339

3440
@dataclass
3541
class Member(ABC):
3642
path: PurePath
3743

44+
@property
45+
def relpath(self) -> PurePath:
46+
return PurePath(*self.path.parts[1:])
47+
48+
def with_base(self, base: PurePath) -> Member:
49+
member = copy.copy(self)
50+
member.path = base / member.relpath
51+
return member
52+
3853

3954
@dataclass
4055
class FileMember(Member):
@@ -53,7 +68,7 @@ class SymlinkMember(Member):
5368

5469
class ArchiveWriter(ABC):
5570
def __init__(self, path: Path):
56-
self.base = PurePath(re.sub('\\.(tar\\.xz|zip)$', '', path.name))
71+
self.base = _path_base(path)
5772
self._members: dict[PurePath, Member] = {}
5873

5974
def __enter__(self) -> Self:
@@ -163,3 +178,101 @@ def close(self) -> None:
163178
elif isinstance(member, SymlinkMember):
164179
raise Exception('Symlinks not supported in Zip')
165180
self._zip.close()
181+
182+
183+
class ArchiveReader(ABC):
184+
def __init__(self, path: Path):
185+
self.base = _path_base(path)
186+
self._tempdir = tempfile.TemporaryDirectory(prefix='openslide-bin-')
187+
self._dir = Path(self._tempdir.name)
188+
189+
@classmethod
190+
@contextmanager
191+
def group(cls, fhs: Iterable[BinaryIO]) -> Iterator[Iterator[MemberSet]]:
192+
with ExitStack() as stack:
193+
readers = [
194+
# mypy thinks we're initializing this ABC, not a subclass
195+
stack.enter_context(cls(fh)) # type: ignore[arg-type]
196+
for fh in fhs
197+
]
198+
yield (MemberSet(members) for members in zip_longest(*readers))
199+
200+
def __enter__(self) -> Self:
201+
return self
202+
203+
def __exit__(
204+
self,
205+
exc_type: type[BaseException] | None,
206+
exc_val: BaseException | None,
207+
exc_tb: TracebackType | None,
208+
) -> None:
209+
self.close()
210+
211+
@abstractmethod
212+
def close(self) -> None:
213+
self._tempdir.cleanup()
214+
215+
@abstractmethod
216+
def __iter__(self) -> Iterator[Member]:
217+
pass
218+
219+
220+
class TarArchiveReader(ArchiveReader):
221+
def __init__(self, fh: BinaryIO):
222+
super().__init__(Path(fh.name))
223+
self._tar = tarfile.open(fileobj=fh)
224+
if hasattr(tarfile, 'data_filter'):
225+
self._tar.extraction_filter = tarfile.data_filter
226+
227+
def close(self) -> None:
228+
self._tar.close()
229+
super().close()
230+
231+
def __iter__(self) -> Iterator[Member]:
232+
while True:
233+
info = self._tar.next()
234+
if info is None:
235+
return
236+
path = PurePath(info.name)
237+
if info.type == tarfile.DIRTYPE:
238+
yield DirMember(path)
239+
elif info.type == tarfile.REGTYPE:
240+
self._tar.extract(info, self._dir)
241+
yield FileMember(path, open(self._dir / path, 'rb'))
242+
elif info.type == tarfile.SYMTYPE:
243+
yield SymlinkMember(path, PurePath(info.linkname))
244+
else:
245+
raise Exception(
246+
f'Unsupported member type: {info.type.decode()}'
247+
)
248+
249+
250+
class MemberSet:
251+
def __init__(self, members: Sequence[Member | None]):
252+
if not all(members):
253+
raise Exception('Missing member in one or more archives')
254+
self.members = cast(Sequence[Member], members)
255+
256+
def __getitem__(self, idx: int) -> Member:
257+
return self.members[idx]
258+
259+
def __iter__(self) -> Iterator[Member]:
260+
return iter(self.members)
261+
262+
@property
263+
def relpaths(self) -> Sequence[PurePath]:
264+
return [member.relpath for member in self]
265+
266+
@cached_property
267+
def datas(self) -> Sequence[bytes]:
268+
ret = []
269+
for member in self:
270+
if not isinstance(member, FileMember):
271+
raise Exception('Member is not a file')
272+
ret.append(member.fh.read())
273+
member.fh.seek(0)
274+
return ret
275+
276+
277+
def _path_base(path: Path) -> PurePath:
278+
return PurePath(re.sub('\\.(tar\\.xz|zip)$', '', path.name))

common/macos.py

+45
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
#
2+
# Tools for building OpenSlide and its dependencies
3+
#
4+
# Copyright (c) 2023 Benjamin Gilbert
5+
# All rights reserved.
6+
#
7+
# This script is free software: you can redistribute it and/or modify it
8+
# under the terms of the GNU Lesser General Public License, version 2.1,
9+
# as published by the Free Software Foundation.
10+
#
11+
# This script is distributed in the hope that it will be useful, but WITHOUT
12+
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
13+
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License
14+
# for more details.
15+
#
16+
# You should have received a copy of the GNU Lesser General Public License
17+
# along with this script. If not, see <http://www.gnu.org/licenses/>.
18+
#
19+
20+
from __future__ import annotations
21+
22+
from collections.abc import Iterable, Sequence
23+
import os
24+
from pathlib import Path
25+
import subprocess
26+
from typing import Any
27+
28+
29+
def merge_macho(paths: Sequence[Path], outdir: Path) -> Path:
30+
outpath = outdir / paths[0].name
31+
args: list[str | Path] = [
32+
os.environ.get('LIPO', 'lipo'),
33+
'-create',
34+
'-output',
35+
outpath,
36+
]
37+
args.extend(paths)
38+
subprocess.check_call(args)
39+
return outpath
40+
41+
42+
def all_equal(items: Iterable[Any]) -> bool:
43+
it = iter(items)
44+
first = next(it)
45+
return all(i == first for i in it)

0 commit comments

Comments
 (0)