Skip to content

Commit

Permalink
initial version of deploy-lipo, implements conan-io#52
Browse files Browse the repository at this point in the history
  • Loading branch information
gmeeker committed Jun 6, 2023
1 parent 1278e27 commit 972a93d
Show file tree
Hide file tree
Showing 6 changed files with 1,260 additions and 0 deletions.
62 changes: 62 additions & 0 deletions extensions/commands/deploy-lipo/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
## Lipo commands

These are commands to deploy macOS or iOS universal binaries.
This wraps around Conan's full_deploy deployer and then runs `lipo` to
produce universal binaries.


#### [Deploy lipo](cmd_deploy_lipo.py)


**Parameters**

The same parameters as `deploy` except multiple profiles should be specified.

```
$ conan deploy-lipo . -pr x8_64 -pr armv8 -b missing -r conancenter
```

This assumes profiles named x86_64 and armv8 with corresponding architectures.
Universal binaries can only have one binary per architecture but each can have
different settings, e.g. minimum deployment OS:

## x86_64
```
[settings]
arch=x86_64
build_type=Release
compiler=apple-clang
compiler.cppstd=gnu17
compiler.libcxx=libc++
compiler.version=14
os=Macos
os.version=10.13
```

## armv8
```
[settings]
arch=armv8
build_type=Release
compiler=apple-clang
compiler.cppstd=gnu17
compiler.libcxx=libc++
compiler.version=14
os=Macos
os.version=11.0
```


#### [Example project](xcode)


```
cd ./xcode
conan deploy-lipo . -pr x86_64 -pr armv8 -b missing
conan build .
```

Verify the architectures:
```
file ./build/Release/example
```
95 changes: 95 additions & 0 deletions extensions/commands/deploy-lipo/_lipo.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import os
import shutil
from subprocess import run


__all__ = ['is_macho_binary', 'lipo']

# These are for optimization only, to avoid unnecessarily reading files.
_binary_exts = ['.a', '.dylib']
_regular_exts = [
'.h', '.hpp', '.hxx', '.c', '.cc', '.cxx', '.cpp', '.m', '.mm', '.txt', '.md', '.html', '.jpg', '.png'
]


def is_macho_binary(filename):
ext = os.path.splitext(filename)[1]
if ext in _binary_exts:
return True
if ext in _regular_exts:
return False
with open(filename, "rb") as f:
header = f.read(4)
if header == b'\xcf\xfa\xed\xfe':
# cffaedfe is Mach-O binary
return True
elif header == b'\xca\xfe\xba\xbe':
# cafebabe is Mach-O fat binary
return True
elif header == b'!<arch>\n':
# ar archive
return True
return False


def copy_arch_file(src, dst, top=None, arch_folders=()):
if os.path.isfile(src):
if top and arch_folders and is_macho_binary(src):
# Try to lipo all available archs on the first path.
src_components = src.split(os.path.sep)
top_components = top.split(os.path.sep)
if src_components[:len(top_components)] == top_components:
paths = [os.path.join(a, *(src_components[len(top_components):])) for a in arch_folders]
paths = [p for p in paths if os.path.isfile(p)]
if len(paths) > 1:
run(['lipo', '-output', dst, '-create'] + paths, check=True)
return
if os.path.exists(dst):
pass # don't overwrite existing files
else:
shutil.copy2(src, dst)


# Modified copytree to copy new files to an existing tree.
def graft_tree(src, dst, symlinks=False, copy_function=shutil.copy2, dirs_exist_ok=False):
names = os.listdir(src)
os.makedirs(dst, exist_ok=dirs_exist_ok)
errors = []
for name in names:
srcname = os.path.join(src, name)
dstname = os.path.join(dst, name)
try:
if symlinks and os.path.islink(srcname):
if os.path.exists(dstname):
continue
linkto = os.readlink(srcname)
os.symlink(linkto, dstname)
elif os.path.isdir(srcname):
graft_tree(srcname, dstname, symlinks, copy_function, dirs_exist_ok)
else:
copy_function(srcname, dstname)
# What about devices, sockets etc.?
# catch the Error from the recursive graft_tree so that we can
# continue with other files
except shutil.Error as err:
errors.extend(err.args[0])
except OSError as why:
errors.append((srcname, dstname, str(why)))
try:
shutil.copystat(src, dst)
except OSError as why:
# can't copy file access times on Windows
if why.winerror is None: # pylint: disable=no-member
errors.extend((src, dst, str(why)))
if errors:
raise shutil.Error(errors)

def lipo(dst_folder, arch_folders):
for folder in arch_folders:
graft_tree(folder,
dst_folder,
symlinks=True,
copy_function=lambda s, d, top=folder: copy_arch_file(s, d,
top=top,
arch_folders=arch_folders),
dirs_exist_ok=True)
60 changes: 60 additions & 0 deletions extensions/commands/deploy-lipo/cmd_deploy_lipo.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import os
import shutil
from subprocess import run

from conan.api.conan_api import ConanAPI
from conan.api.output import ConanOutput
from conan.cli.command import conan_command
from conan.cli.args import common_graph_args
from conan.errors import ConanException

from _lipo import lipo as lipo_folder


_valid_archs = [
'x86',
'x86_64',
'armv7',
'armv8',
'armv8_32',
'armv8.3',
'armv7s',
'armv7k'
]

@conan_command(group="Consumer")
def deploy_lipo(conan_api: ConanAPI, parser, *args):
"""
Deploy dependencies for multiple profiles and lipo into universal binaries
"""
common_graph_args(parser)
parsed = parser.parse_args(*args)
profiles = parsed.profile_host or parsed.profile
if not profiles:
raise ConanException("Please provide profiles with -pr or -pr:h")
other_args = []
i = 0
while i < len(args[0]):
arg = args[0][i]
if arg in ['-pr', '-pr:h', '--profile', '--profile:host']:
i += 2
else:
other_args.append(arg)
i += 1
for profile in profiles:
run(['conan', 'install',
'--deploy=full_deploy',
'-pr:h', profile
] + other_args)
output_dir = os.path.join('full_deploy', 'host')
for package in os.listdir(output_dir):
package_dir = os.path.join(output_dir, package)
for version in os.listdir(package_dir):
version_dir = os.path.join(package_dir, version)
for build in os.listdir(version_dir):
d = os.path.join(version_dir, build)
archs = [os.path.join(d, x) for x in os.listdir(d) if x in _valid_archs]
# We could skip if len(archs) == 1 but the dir layout would be different
lipo_folder(d, archs)
for arch in archs:
shutil.rmtree(arch)
15 changes: 15 additions & 0 deletions extensions/commands/deploy-lipo/xcode/conanfile.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
from conan import ConanFile
from conan.tools.apple import XcodeBuild


class UniversalRecipe(ConanFile):
# Note that we don't depend on arch
settings = "os", "compiler", "build_type"
requires = ("libtiff/4.5.0",)

def build(self):
# xcodebuild = XcodeBuild(self)
# Don't use XcodeBuild because it passes a single -arch flag
build_type = self.settings.get_safe("build_type")
project = 'example.xcodeproj'
self.run('xcodebuild -configuration {} -project {} -alltargets'.format(build_type, project))
Loading

0 comments on commit 972a93d

Please sign in to comment.