Skip to content

Commit d672b91

Browse files
committed
Build platform-specific wheels containing libmagic
1 parent 2a01b18 commit d672b91

File tree

6 files changed

+255
-47
lines changed

6 files changed

+255
-47
lines changed

.github/workflows/main.yml

+134
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
name: GH
2+
3+
permissions:
4+
contents: write
5+
6+
on:
7+
pull_request:
8+
push:
9+
branches: master
10+
release:
11+
types: [released, prereleased]
12+
workflow_dispatch: # allows running workflow manually from the Actions tab
13+
14+
jobs:
15+
16+
build-sdist:
17+
runs-on: ubuntu-latest
18+
19+
env:
20+
PIP_DISABLE_PIP_VERSION_CHECK: 1
21+
22+
steps:
23+
- uses: actions/checkout@v3
24+
with:
25+
fetch-depth: 0
26+
27+
- name: Set up Python
28+
uses: actions/setup-python@v4
29+
with:
30+
python-version: '3.x'
31+
32+
- run: sudo apt-get install -y libmagic1
33+
34+
- name: Build source distribution
35+
run: |
36+
pip install -U setuptools wheel pip
37+
python setup.py sdist
38+
39+
- uses: actions/upload-artifact@v3
40+
with:
41+
name: dist
42+
path: dist/*.tar.*
43+
44+
45+
build-wheels-matrix:
46+
runs-on: ubuntu-latest
47+
outputs:
48+
include: ${{ steps.set-matrix.outputs.include }}
49+
steps:
50+
- uses: actions/checkout@v3
51+
- uses: actions/setup-python@v4
52+
with:
53+
python-version: '3.x'
54+
- run: pip install cibuildwheel==2.15.0
55+
- id: set-matrix
56+
env:
57+
CIBW_PROJECT_REQUIRES_PYTHON: '==3.8.*'
58+
run: |
59+
MATRIX_INCLUDE=$(
60+
{
61+
cibuildwheel --print-build-identifiers --platform linux --arch x86_64,aarch64,i686 | grep cp | jq -nRc '{"only": inputs, "os": "ubuntu-latest"}' \
62+
&& cibuildwheel --print-build-identifiers --platform macos --arch x86_64,arm64 | grep cp | jq -nRc '{"only": inputs, "os": "macos-11"}' \
63+
&& cibuildwheel --print-build-identifiers --platform windows --arch x86,AMD64 | grep cp | jq -nRc '{"only": inputs, "os": "windows-latest"}'
64+
} | jq -sc
65+
)
66+
echo "include=$MATRIX_INCLUDE" >> $GITHUB_OUTPUT
67+
68+
69+
build-wheels:
70+
needs: build-wheels-matrix
71+
runs-on: ${{ matrix.os }}
72+
name: Build ${{ matrix.only }}
73+
74+
strategy:
75+
fail-fast: false
76+
matrix:
77+
include: ${{ fromJson(needs.build-wheels-matrix.outputs.include) }}
78+
79+
steps:
80+
- uses: actions/checkout@v3
81+
with:
82+
fetch-depth: 0
83+
84+
- name: Set up QEMU
85+
if: runner.os == 'Linux'
86+
uses: docker/setup-qemu-action@v2
87+
88+
- uses: pypa/[email protected]
89+
timeout-minutes: 10
90+
with:
91+
only: ${{ matrix.only }}
92+
env:
93+
CIBW_BUILD_VERBOSITY: 1
94+
CIBW_BEFORE_BUILD: 'bash -c "make install_libmagic"'
95+
96+
- uses: actions/upload-artifact@v3
97+
with:
98+
name: dist
99+
path: wheelhouse/*.whl
100+
101+
102+
publish:
103+
needs: [build-sdist, build-wheels]
104+
if: github.event_name == 'release'
105+
runs-on: ubuntu-latest
106+
107+
steps:
108+
- uses: actions/setup-python@v4
109+
with:
110+
python-version: 3.x
111+
112+
- uses: actions/download-artifact@v3
113+
with:
114+
name: dist
115+
path: dist/
116+
117+
- run: ls -ltra dist/
118+
119+
- run: pip install -U twine python-magic --find-links ./dist
120+
121+
- name: Smoketest
122+
run: python -c "import magic; magic.Magic()"
123+
124+
- name: Upload release assets
125+
uses: softprops/[email protected]
126+
with:
127+
files: dist/*
128+
129+
- name: Upload to PyPI
130+
env:
131+
TWINE_USERNAME: __token__
132+
TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }}
133+
run: |
134+
twine upload dist/*

Makefile

+20
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
SHELL := /bin/bash
2+
3+
.PHONY: install_libmagic
4+
## Install libmagic
5+
install_libmagic:
6+
# Debian https://packages.ubuntu.com/libmagic1
7+
# RHEL https://git.almalinux.org/rpms/file
8+
# Mac https://formulae.brew.sh/formula/libmagic
9+
# Windows https://github.com/julian-r/file-windows
10+
( ( ( brew install libmagic || ( apt-get update && apt-get install -y libmagic1 ) ) || apk add --update libmagic ) || yum install file-libs ) || ( python -c 'import platform, sysconfig, io, zipfile, urllib.request; assert platform.system() == "Windows"; machine = "x86" if sysconfig.get_platform() == "win32" else "x64"; print(machine); zipfile.ZipFile(io.BytesIO(urllib.request.urlopen(f"https://github.com/julian-r/file-windows/releases/download/v5.44/file_5.44-build104-vs2022-{machine}.zip").read())).extractall(".")' && ls -ltra )
11+
# on cibuildwheel, the lib needs to exist in the project before running setup.py
12+
python -c "import subprocess; from magic.loader import load_lib; lib = load_lib()._name; print(f'linking {lib}'); subprocess.check_call(['cp', lib, 'magic'])"
13+
cp /usr/share/misc/magic.mgc magic || true # only on linux
14+
ls -ltra magic
15+
16+
.DEFAULT_GOAL := help
17+
.PHONY: help
18+
## Print Makefile documentation
19+
help:
20+
@perl -0 -nle 'printf("\033[36m %-15s\033[0m %s\n", "$$2", "$$1") while m/^##\s*([^\r\n]+)\n^([\w.-]+):[^=]/gm' $(MAKEFILE_LIST) | sort

README.md

+25-12
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,7 @@ will fail throw if this is attempted.
3030
```python
3131
>>> f = magic.Magic(uncompress=True)
3232
>>> f.from_file('testdata/test.gz')
33-
'ASCII text (gzip compressed data, was "test", last modified: Sat Jun 28
34-
21:32:52 2008, from Unix)'
33+
'ASCII text (gzip compressed data, was "test", last modified: Sat Jun 28 21:32:52 2008, from Unix)'
3534
```
3635

3736
You can also combine the flag options:
@@ -53,26 +52,40 @@ Other sources:
5352
- GitHub: https://github.com/ahupp/python-magic
5453

5554
This module is a simple wrapper around the libmagic C library, and
56-
that must be installed as well:
55+
comes bundled in the wheels on PyPI. For systems not supported by the wheels, libmagic
56+
needs to be installed before installing this library:
5757

58-
### Debian/Ubuntu
58+
### Linux
5959

60-
```
61-
sudo apt-get install libmagic1
60+
The Linux wheels should run on most systems out of the box.
61+
62+
Depending on your system and CPU architecture, there might be no compatible wheel uploaded. However, precompiled libmagic might still be available for your system:
63+
64+
```sh
65+
# Debian/Ubuntu
66+
apt-get update && apt-get install -y libmagic1
67+
# Alpine
68+
apk add --update libmagic
69+
# RHEL
70+
yum install file-libs
6271
```
6372

6473
### Windows
6574

66-
You'll need DLLs for libmagic. @julian-r maintains a pypi package with the DLLs, you can fetch it with:
75+
The DLLs that are bundled in the Windows wheels are compiled by @julian-r and hosted at https://github.com/julian-r/file-windows/releases.
6776

68-
```
69-
pip install python-magic-bin
70-
```
77+
For ARM64 Windows, you'll need to compile libmagic from source.
7178

7279
### OSX
7380

74-
- When using Homebrew: `brew install libmagic`
75-
- When using macports: `port install file`
81+
The Mac wheels are compiled on GitHub Actions using `macos-11` runners. For older Macs, you'll need to install libmagic from source:
82+
83+
```sh
84+
# homebrew
85+
brew install libmagic
86+
# macports
87+
port install file
88+
```
7689

7790
### Troubleshooting
7891

magic/__init__.py

+6-1
Original file line numberDiff line numberDiff line change
@@ -330,7 +330,12 @@ def magic_descriptor(cookie, fd):
330330

331331

332332
def magic_load(cookie, filename):
333-
return _magic_load(cookie, coerce_filename(filename))
333+
try:
334+
return _magic_load(cookie, coerce_filename(filename))
335+
except MagicException:
336+
# wheels package the mime database in this directory
337+
filename = os.path.join(os.path.dirname(__file__), 'magic.mgc')
338+
return _magic_load(cookie, coerce_filename(filename))
334339

335340

336341
magic_setflags = libmagic.magic_setflags

magic/loader.py

+42-31
Original file line numberDiff line numberDiff line change
@@ -1,50 +1,61 @@
11
from ctypes.util import find_library
22
import ctypes
3-
import sys
43
import glob
54
import os.path
5+
import subprocess
6+
import sys
67

78
def _lib_candidates():
9+
here = os.path.dirname(__file__)
810

9-
yield find_library('magic')
11+
if sys.platform == 'darwin':
1012

11-
if sys.platform == 'darwin':
13+
paths = [
14+
here,
15+
os.path.abspath("."),
16+
'/opt/local/lib',
17+
'/usr/local/lib',
18+
'/opt/homebrew/lib',
19+
] + glob.glob('/usr/local/Cellar/libmagic/*/lib')
1220

13-
paths = [
14-
'/opt/local/lib',
15-
'/usr/local/lib',
16-
'/opt/homebrew/lib',
17-
] + glob.glob('/usr/local/Cellar/libmagic/*/lib')
21+
for i in paths:
22+
yield os.path.join(i, 'libmagic.dylib')
1823

19-
for i in paths:
20-
yield os.path.join(i, 'libmagic.dylib')
24+
elif sys.platform in ('win32', 'cygwin'):
2125

22-
elif sys.platform in ('win32', 'cygwin'):
26+
prefixes = ['libmagic', 'magic1', 'magic-1', 'cygmagic-1', 'libmagic-1', 'msys-magic-1']
2327

24-
prefixes = ['libmagic', 'magic1', 'magic-1', 'cygmagic-1', 'libmagic-1', 'msys-magic-1']
28+
for i in prefixes:
29+
# find_library searches in %PATH% but not the current directory,
30+
# so look for both
31+
yield os.path.join(here, '%s.dll' % i)
32+
yield os.path.join(os.path.abspath("."), '%s.dll' % i)
33+
yield find_library(i)
2534

26-
for i in prefixes:
27-
# find_library searches in %PATH% but not the current directory,
28-
# so look for both
29-
yield './%s.dll' % (i,)
30-
yield find_library(i)
35+
elif sys.platform == 'linux':
36+
# on some linux systems (musl/alpine), find_library('magic') returns None
37+
yield subprocess.check_output(
38+
"( ldconfig -p | grep 'libmagic.so.1' | grep -o '/.*' ) || echo '/usr/lib/libmagic.so.1'",
39+
shell=True,
40+
universal_newlines=True,
41+
).strip()
42+
yield os.path.join(here, 'libmagic.so.1')
43+
yield os.path.join(os.path.abspath("."), 'libmagic.so.1')
3144

32-
elif sys.platform == 'linux':
33-
# This is necessary because alpine is bad
34-
yield 'libmagic.so.1'
45+
yield find_library('magic')
3546

3647

3748
def load_lib():
3849

39-
for lib in _lib_candidates():
40-
# find_library returns None when lib not found
41-
if lib is None:
42-
continue
43-
try:
44-
return ctypes.CDLL(lib)
45-
except OSError:
46-
pass
47-
else:
48-
# It is better to raise an ImportError since we are importing magic module
49-
raise ImportError('failed to find libmagic. Check your installation')
50+
for lib in _lib_candidates():
51+
# find_library returns None when lib not found
52+
if lib is None:
53+
continue
54+
try:
55+
return ctypes.CDLL(lib)
56+
except OSError as exc:
57+
pass
58+
else:
59+
# It is better to raise an ImportError since we are importing magic module
60+
raise ImportError('failed to find libmagic. Check your installation')
5061

setup.py

+28-3
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,40 @@
44
import setuptools
55
import io
66
import os
7+
import sys
78

9+
# python packages should not install succesfully if libraries are missing
10+
from magic.loader import load_lib
11+
lib = load_lib()._name
812

913
def read(file_name):
1014
"""Read a text file and return the content as a string."""
1115
with io.open(os.path.join(os.path.dirname(__file__), file_name),
1216
encoding='utf-8') as f:
1317
return f.read()
1418

19+
def get_cmdclass():
20+
"""Build a forward compatible ABI3 wheel when `setup.py bdist_wheel` is called."""
21+
if sys.version_info[0] == 2:
22+
return {}
23+
24+
try:
25+
from wheel.bdist_wheel import bdist_wheel
26+
except ImportError:
27+
return {}
28+
29+
class bdist_wheel_abi3(bdist_wheel):
30+
def get_tag(self):
31+
python, abi, _ = super().get_tag()
32+
# get the platform tag based on libmagic included in this wheel
33+
self.root_is_pure = False
34+
_, _, plat = super().get_tag()
35+
return python, abi, plat
36+
37+
return {"bdist_wheel": bdist_wheel_abi3}
38+
39+
cmdclass = get_cmdclass()
40+
1541
setuptools.setup(
1642
name='python-magic',
1743
description='File type identification using libmagic',
@@ -22,9 +48,8 @@ def read(file_name):
2248
long_description=read('README.md'),
2349
long_description_content_type='text/markdown',
2450
packages=['magic'],
25-
package_data={
26-
'magic': ['py.typed', '*.pyi', '**/*.pyi'],
27-
},
51+
package_data={'magic': ['py.typed', '*.pyi', '*.dylib*', '*.dll', '*.so*', 'magic.mgc']},
52+
cmdclass=cmdclass,
2853
keywords="mime magic file",
2954
license="MIT",
3055
python_requires='>=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*',

0 commit comments

Comments
 (0)