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

Add details on native packaging requirements exposed by mobile platforms #27

Open
wants to merge 31 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
eb2419a
Add details on native packaging requirements exposed by mobile platfo…
freakboy3742 Jan 9, 2023
8fef63e
Clarified the role/impact of cross-compilation on non-macOS platforms.
freakboy3742 Jan 10, 2023
d16035f
Grammar cleanup.
freakboy3742 Jan 10, 2023
84dbd5f
Add note about Windows platform support
freakboy3742 Jan 10, 2023
2a40f47
Moved a paragraph about the universal2 to current state.
freakboy3742 Jan 10, 2023
2563270
Clarified how Android deals with dependencies.
freakboy3742 Jan 10, 2023
b9b904c
Added an alternative approach for handling iOS multi-arch.
freakboy3742 Jan 10, 2023
45f748f
Modified comments to use common section structure, and include specif…
freakboy3742 Jan 16, 2023
373bb09
Apply suggestions from code review
freakboy3742 Jan 16, 2023
d8a2ca6
More updates stemming from review.
freakboy3742 Jan 16, 2023
f533395
Expand note about Linux support.
freakboy3742 Jan 17, 2023
8475360
Correct an it's typo.
freakboy3742 Jan 17, 2023
2886f2c
Add content to page on cross compilation
rgommers Feb 27, 2023
7556850
Resolve the last cross-compilation comment, on `pip --platform`
rgommers Mar 10, 2023
cb85652
Merge branch 'main' into mobile-details
rgommers Mar 10, 2023
49806e2
Put back link to "multiple architectures" page from cross compile page
rgommers Mar 10, 2023
ea1fb60
Remove the `cross_platform.md` file
rgommers Mar 10, 2023
d249af6
Fix some formatting and typo issues
rgommers Mar 10, 2023
50d8c26
Revisions to multi-architecture notes following review.
freakboy3742 Mar 20, 2023
a9776e0
Add foldout for pros and cons of `universal2` wheels
rgommers Mar 21, 2023
8d46e06
Add the 'for' arguments for universal2.
freakboy3742 Mar 21, 2023
5d06a56
Clarified 'end user' language; added note about merge problems.
freakboy3742 Mar 22, 2023
3e1fc05
Clarify the state of arm64 on github actions.
freakboy3742 Mar 22, 2023
74705d8
Add reference to pip issue about universal2 wheel installation.
freakboy3742 Mar 22, 2023
f46d2b0
Fixed typo.
freakboy3742 Mar 22, 2023
e1c278f
Removed subjective language.
freakboy3742 Mar 22, 2023
1a926eb
Apply textual/typo suggestions
rgommers Mar 22, 2023
7967383
Rephrase universal2 usage frequency/demand phrasing
rgommers Mar 22, 2023
1fb0ffb
Tone down the statement on "must provide thin wheels"
rgommers Mar 22, 2023
b44a322
Rephrase note on needed robustness improvements in delocate-fuse
rgommers Mar 22, 2023
dd93f1f
Add "first-class support for fusing thin wheels" as a potential solution
rgommers Mar 22, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/index.md
Expand Up @@ -67,6 +67,8 @@ workarounds for.
4. [Metadata handling on PyPI](key-issues/pypi_metadata_handling.md)
5. [Distributing a package containing SIMD code](key-issues/simd_support.md)
6. [Unsuspecting users getting failing from source builds](key-issues/unexpected_fromsource_builds.md)
7. [Platforms with multiple CPU architectures](key-issues/multiple_architectures.md)
8. [Cross-platform installation](key-issues/cross_platform.md)


## Contributing
Expand Down
33 changes: 33 additions & 0 deletions docs/key-issues/cross_platform.md
@@ -0,0 +1,33 @@
# Cross-platform installation

The historical assumption of compilation is that the platform where the code is
compiled will be the same as the platform where the final code will be executed
(if not literally the same machine, then at least one that is CPU and ABI
compatible at the operating system level). This is a reasonable assumption for
most desktop projects; However, for mobile platforms, this isn't the case.

On mobile platforms, an app is compiled on a desktop platform, and transferred
to the mobile device (or a simulator) for testing. The compiler is not executed
on device. Therefore, it must be possible to build a binary artefact for a CPU
architecture and a ABI that is different the platform that is running the
compiler.

freakboy3742 marked this conversation as resolved.
Show resolved Hide resolved
A microcosm of this problem exists on macOS as a result of the Apple Silicon
transition. Most CI systems don't provide native ARM hardware, but most
developers will still want ARM64-compatible build artefacts. Apple has provided
the tools compile [fat binaries](multiple_architectures.md) on x86_64 hardware;
however, in this case, the host platform (macOS on x86_64) will still be one of
the outputs of the compilation process. For mobile platforms, the computer that
compiles the code will not be able to execute the code that has been compiled.
rgommers marked this conversation as resolved.
Show resolved Hide resolved

## Potential solutions or mitigations

Compiler and build toolchains (e.g., autoconf/automake) have long supported
cross-compilation; however, these cross-compilation capabilities are easy to
break unless they are exercised regularly.

In the Python space, tools like [crossenv](https://github.com/benfogle/crossenv)
also exist; these tools use a collection of path hacks and overrides of known
sources of platform-specific details (like `distutils`) to provide a
cross-compilation environment. However, these solutions tend to be somewhat
fragile as aren't first-class citizens of the Python ecosystem.
rgommers marked this conversation as resolved.
Show resolved Hide resolved
129 changes: 129 additions & 0 deletions docs/key-issues/multiple_architectures.md
@@ -0,0 +1,129 @@
# Platforms with multiple CPU architectures

In addition to any ABI requirements, a binary is compiled for a CPU
architecture. That CPU architecture defines the CPU instructions that can be
issued by the binary.
rgommers marked this conversation as resolved.
Show resolved Hide resolved

Historically, it could be assumed that an executable or library would be
compiled for a single CPU archicture. On the rare occasion that an operating
system was available for mulitple CPU architectures, it became the
rgommers marked this conversation as resolved.
Show resolved Hide resolved
responsibility of the user to find (or compile) a binary that was compiled for
their host CPU architecture.
rgommers marked this conversation as resolved.
Show resolved Hide resolved

However, on occasion, we see an operating system platform where multiple CPU
architectures are supported:

* In the early days of Windows NT, both x86 and DEC Alpha CPUs were supported
* Although Linux started as an x86 project, the Linux kernel is now available a
wide range of other CPU architectures, including ARM64, RISC-V, PowerPC, s390
and more.
freakboy3742 marked this conversation as resolved.
Show resolved Hide resolved
* Apple transitioned Mac hardware from PowerPC to Intel (x86-64) CPUs, providing
a forwards compatibility path for binaries
* Apple is currently transitioning Mac hardware from Intel (x86-64) to
Apple Silicon (ARM64) CPUs, again providing a forwards compatibility
path
* Apple supports ARMv6, ARMv7, ARMv7s, ARM64 and ARM64e on iOS
* Android currently supports ARMv7, ARM64, x86, and x86-64; it has historically
also supported ARMv5 and MIPS
rgommers marked this conversation as resolved.
Show resolved Hide resolved

CPU architecture compatibility is a necessary, but not sufficient criterion for
determining binary compatibility. Even if two binaries are compiled for the same
CPU architecture, that doesn't guarantee [ABI compatibility](abi.md).

In some respects, CPU architecture compatibility could be considered a superset
rgommers marked this conversation as resolved.
Show resolved Hide resolved
of [GPU compatibility](gpus.md). When dealing with multiple CPU architectures,
there may be some overal with the solutions that can be used to support GPUs in
native binaries.
freakboy3742 marked this conversation as resolved.
Show resolved Hide resolved

## Platform approaches for dealing with multiple architectures

Three approaches have emerged for handling multiple CPU architectures.

### Multiple binaries

The minimal solution is to distribute multiple binaries. This is the approach
that was used by Windows NT, and is currently supported by Linux. At time of
distribution, an installer or other downloadable artefact is provided for each
supported platform, and it is up to the user to select and download the correct
artefact.

### Archiving

The approach taken by Android is very similar to the multiple binary approach,
with some affordances and tooling to simplify distribution.

When building an Android project, each target architecture is compiled
independently. If a native binary library is required to compile the Android
application, a version must be provided for each supported CPU architecture. A
directory layout convention exists for providing a binary for each platform,
with the same library name. This yields an independent final binary (APK) for
each CPU architecture. When running locally, a CPU-specific APK will be
uploaded to the simulator or test device.
rgommers marked this conversation as resolved.
Show resolved Hide resolved

To simplify the process of distributing the application, at time of publication,
a single Android App Bundle (AAB) is generated from the multiple CPU-specific
APKs. This AAB contains binaries for all platforms that can be uploaded to an
app store.
rgommers marked this conversation as resolved.
Show resolved Hide resolved

When an end-user requests the installation of an app, the app store strips out the
binary that is appropriate for the end-user's device.
Copy link
Member

Choose a reason for hiding this comment

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

This seems like a nice solution. After reading this, I'm wondering what the actual problem here is from Beeware's perspective?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

In the Android case, it's not really a problem per se; its more of a "consideration to keep in mind".

Let's say I have an Android project that uses a Python package A that has a binary module; that package has a dependency on B (which also has a binary module). When I do the compilation pass for ARM64, I pip install A, which does a dependency resolution pass to determine a compatible version of B. I then do a compilation pass for ARMv7, which does a separate dependency resolution pass to determine a compatible version of B. The use of a separate dependency resolution pass introduces the possibility that my ARMv7 and ARM64 binaries have entirely different versions of package B (and, I guess, potentially package A as well).

That's not a problem; it could even be argued as "working as designed". It's how BeeWare (strictly, the Chaquopy subsystem that Briefcase uses) is working at present; it just might be a little surprising if you don't have a systematic way of at least flagging that there are different versions installed for each platform. I'll modify the wording to clarify this.

However, for the same project on iOS, this isn't an option. iOS apps perform one compilation pass per ABI (i.e. one for the simulator, and one for the physical device), so the final installed artefact needs to contain a single fat binary with all the architectures. This could be treated as an install-time problem rather than distribution problem, though; I'll add a note about that possible approach.

Copy link
Member

Choose a reason for hiding this comment

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

Thanks, this is a good and concrete example. It sounds like it's only a problem with pip/PyPI - any other package relevant package manager gets all the metadata first and then does a single solve for all dependencies, so you'd get a single version of package B.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'm not sure I follow why it's only a pip/PyPI problem. The issue isn't the solution path; it's how many times you need to run the installer (and thus the solver). If I solve and install for arm64, then solve and install for x86-64, that's 2 independent solutions, and any inconsistency in availability of packages for arm64 and x86-64 will result in different solutions. AFAICT, this will still be an issue for conda, as the issue isn't the availability of metadata for a single platform; it's the availability of the solution arrived at by a completely independent solver pass.

You can only avoid this problem if you do a single resolver pass looking to satisfy both architectures at the same time, and only install once a single solution that works for both architectures is found (or, I guess, you have some ways to pass a specific solution found by pass 1 into subsequent passes in a way that doesn't include package hashes, as you'll be installing a different package with the same name and version, but different ABI/architecture).

Copy link
Member

Choose a reason for hiding this comment

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

Ah okay, thank makes sense, thanks.

Copy link

Choose a reason for hiding this comment

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

as the issue isn't the availability of metadata for a single platform; it's the availability of the solution arrived at by a completely independent solver pass.

conda metadata is available offline, unlike pip where every resolver run is a series of API calls followed by downloading artifacts followed by extracting artifacts followed by yet more API calls. So you actually can guarantee multiple runs of the resolver will produce the same results.

If the metadata is guaranteed to be consistent across architectures at any given moment (e.g. packages of any given version will always exist for all architectures) then you can also get the same resolver result across architectures too. That's a social problem -- it depends on whether the ecosystem allows "partial releases".

or, I guess, you have some ways to pass a specific solution found by pass 1 into subsequent passes in a way that doesn't include package hashes

pip freeze?

Copy link
Member

Choose a reason for hiding this comment

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

So you actually can guarantee multiple runs of the resolver will produce the same results.

In principle, yes. But I don't think either conda or mamba have this capability, so in practice it'd be pretty hard today.

Copy link
Contributor

@mhsmith mhsmith Jan 10, 2023

Choose a reason for hiding this comment

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

Chaquopy actually doesn't run multiple dependency resolution passes; it works like this:

  • Run pip normally for the first architecture: it doesn't matter which one.
  • Use the .dist-info metadata to identify the native packages: both those directly requested by the user, and those installed as indirect requirements by pip.
  • Separate the native packages from the the pure-Python packages.
  • Run pip again for each of the remaining architectures. But this time, we install only the native packages, pin them all to the same versions that pip selected for the first architecture, and add the option --no-deps.

So if a package isn't available in the same version for all of the app's architectures, the build will fail. Since we build all our Android wheels ourselves, this hasn't yet been an issue.

The end result is one directory tree for pure-Python packages, and one directory tree per architecture for native packages, all of which are guaranteed to have the same versions. Those directory trees are then packaged into the APK in a way that allows them to be efficiently accessed at runtime.


### Fat binaries

Apple has taken the approach of "fat" binaries. A fat binary is a single
executable or library artefact that contains code for multiple CPU
architectures.

Fat binaries can be compiled in two ways:

1. **Single pass** Apple has modified their compiler tooling with flags that
allow the user to specify a single compilation command, and instruct the
compiler to generate multiple output architectures in the output binary
2. **Multiple pass** After compiling a binary for each platform, Apple provides
a call named `lipo` to combine multiple single-architecture binaries into a
single fat binary that contains all platforms.
rgommers marked this conversation as resolved.
Show resolved Hide resolved

At runtime, the operating system loads the binary slice for the current CPU
architecture, and the linker loads the appropriate slice from the fat binary of
any dynamic libraries.

On macOS ARM hardware, Apple also provides Rosetta as a support mechanism; if a
user tries to run an binary that doesn't contain an ARM64 slice, but *does*
rgommers marked this conversation as resolved.
Show resolved Hide resolved
contain an x86-64 slice, the x86-64 slice will be converted at runtime into an
ARM64 binary. Complications can occur when only *some* of the binary is being
converted (e.g., if the binary being executed is fat, but a dynamic library
isn't).

iOS has an additional complication of requiring support for mutiple *ABIs* in
rgommers marked this conversation as resolved.
Show resolved Hide resolved
addition to multiple CPU archiectures. The ABI for the iOS simulator and
physical iOS devices are different; however, ARM64 is a supported CPU
architecture for both. As a result, it is not possible to produce a single fat
library that supports both the iOS simulator and iOS devices. Apple provides an
additional structure - the `XCFramework` - as a wrapper format for packaging
libraries that need to span multiple ABIs. When developing an application for
iOS, a developer will need to install binaries for both the simulator and
physical devices.

## Potential solutions or mitigations

Python currently provides `universal2` wheels to support x86_64 and ARM64 in a
single wheel. This is effectively a "fat wheel" format; the `.dylib` files
contained in the wheel are fat binaries containing both x86_64 and ARM64 slices.
rgommers marked this conversation as resolved.
Show resolved Hide resolved

However, "Universal2" is a macOS-specific definition that encompasses the scope
of the specific "Apple Silicon" transition ("Universal" wheels also existed
historically for the PowerPC to Intel transition). Even inside the Apple
ecosystem, iOS, tvOS, and watchOS all have different combinations of supported
CPU architectures.

A more general solution for naming multi-architecture binaries, similar to how a
wheel can declare compatibility with multiple CPython versions (e.g.,
`cp34.cp35.cp36-abi3-manylinux1_x86_64`) may be called for. In such a scheme,
`cp310-abi3-macosx_10_9_universal2` would be equivalent to
`cp310-abi3-macosx_10_9_x86_64.arm64`.

To support Android's multi-architecture approach, it may be necessary to extend
installation tools to allow for installing multiple versions of a wheel in one
installation pass. This can be emulated by making multiple independent calls to
to package installer tools; but that results in independent dependency
resolution, etc.
4 changes: 3 additions & 1 deletion mkdocs.yml
Expand Up @@ -20,7 +20,7 @@ theme:
- scheme: default
primary: blue grey
toggle:
icon: material/brightness-7
icon: material/brightness-7
name: Switch to dark mode

nav:
Expand All @@ -42,6 +42,8 @@ nav:
- 'key-issues/pypi_metadata_handling.md'
- 'key-issues/simd_support.md'
- 'key-issues/unexpected_fromsource_builds.md'
- 'key-issues/multiple_architectures.md'
- 'key-issues/cross_platform.md'
- 'other_issues.md'
- 'Background':
- 'background/binary_interface.md'
Expand Down