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

Build platform-specific wheels containing libmagic #294

Open
wants to merge 38 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
ec952d7
Build platform-specific wheels containing libmagic
ddelange Sep 7, 2023
a437409
Move persmissions into job scope, remove ABI3 reference
ddelange Sep 15, 2023
4a715e2
Switch to PyPI trusted publishing
ddelange Nov 13, 2023
1adc0a5
Add CIBW_TEST_COMMAND and indent Makefile
ddelange Jan 25, 2024
20e2dc9
Merge branch 'master' of https://github.com/ahupp/python-magic into a…
ddelange Jan 25, 2024
090b1d4
Fix CI for macos
ddelange Jan 25, 2024
20d8fee
Add dependabot.yml
ddelange Jan 25, 2024
85d4422
Migrate actions/upload-artifact@v4
ddelange Jan 25, 2024
2efa36d
Ensure magic.mgc packaged in wheel gets recognised
ddelange Jan 25, 2024
0b43bc6
Add note about --no-binary to the installation instructions
ddelange Apr 1, 2024
05df4f9
Separate mac versions
ddelange Apr 4, 2024
d2972b9
Update cibuildwheel
ddelange Apr 4, 2024
e182ae1
Bump pypa/[email protected]
ddelange Apr 4, 2024
94718d5
Bump cibuildwheel docker images
ddelange Apr 11, 2024
359e007
Revert "Bump cibuildwheel docker images"
ddelange Apr 11, 2024
bb9c685
Move magic.mgc injection into Magic class
ddelange Apr 11, 2024
b0fddf3
Build on more recent cibw images
ddelange Apr 11, 2024
dc075e9
Use hls mp4 (recent libmagic only) for testing
ddelange Apr 11, 2024
144132d
Revert "Use hls mp4 (recent libmagic only) for testing"
ddelange Apr 11, 2024
fe62a26
Install from source
ddelange Apr 23, 2024
f7bbb03
Documentation and readability
ddelange Apr 24, 2024
2e6104e
Build macos wheels with maximum backwards compatibility
ddelange May 6, 2024
ca4def3
Use CIBW_SKIP
ddelange May 6, 2024
ba87ffd
Apply suggestions from code review
ddelange May 20, 2024
e112de3
Merge branch 'master' of ahupp/python-magic into abi3-wheels
ddelange May 22, 2024
eba05b6
Fix compat.py now that bundled libmagic is preferred
ddelange May 22, 2024
8381a96
Fix https://github.com/ahupp/python-magic/issues/321
ddelange May 22, 2024
9c5f955
Use sudo on ubuntu-latest in ci.yml
ddelange May 22, 2024
e6d5ed0
Fix sudo not available on windows-latest
ddelange May 22, 2024
50504a2
Merge branch 'ahupp:master' into abi3-wheels
ddelange May 22, 2024
9357f27
Add entries in CHANGELOG
ddelange May 23, 2024
53d099b
Merge branch 'master' of https://github.com/ahupp/python-magic into a…
ddelange May 26, 2024
9bf2e9c
Fix test
ddelange May 26, 2024
f7341ce
PR Suggestions
ddelange May 26, 2024
da5b330
Apply suggestions from code review
ddelange May 28, 2024
258efa4
Revert partially: fix install on Windows
ddelange May 29, 2024
3a55538
Merge branch 'master' into abi3-wheels
ddelange Jun 18, 2024
65fb61c
Apply suggestions from code review
ddelange Jun 26, 2024
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
134 changes: 134 additions & 0 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
name: GH

permissions:
contents: write
ddelange marked this conversation as resolved.
Show resolved Hide resolved

on:
pull_request:
push:
branches: master
release:
types: [released, prereleased]
workflow_dispatch: # allows running workflow manually from the Actions tab

jobs:

build-sdist:
runs-on: ubuntu-latest

env:
PIP_DISABLE_PIP_VERSION_CHECK: 1

steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0

- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: '3.x'

- run: sudo apt-get install -y libmagic1

- name: Build source distribution
run: |
pip install -U setuptools wheel pip
ddelange marked this conversation as resolved.
Show resolved Hide resolved
python setup.py sdist
- uses: actions/upload-artifact@v3
with:
name: dist
path: dist/*.tar.*


build-wheels-matrix:
runs-on: ubuntu-latest
outputs:
include: ${{ steps.set-matrix.outputs.include }}
steps:
- uses: actions/checkout@v3
- uses: actions/setup-python@v4
with:
python-version: '3.x'
- run: pip install cibuildwheel==2.15.0
- id: set-matrix
env:
CIBW_PROJECT_REQUIRES_PYTHON: '==3.8.*'
run: |
MATRIX_INCLUDE=$(
{
cibuildwheel --print-build-identifiers --platform linux --arch x86_64,aarch64,i686 | grep cp | jq -nRc '{"only": inputs, "os": "ubuntu-latest"}' \
&& cibuildwheel --print-build-identifiers --platform macos --arch x86_64,arm64 | grep cp | jq -nRc '{"only": inputs, "os": "macos-11"}' \
&& cibuildwheel --print-build-identifiers --platform windows --arch x86,AMD64 | grep cp | jq -nRc '{"only": inputs, "os": "windows-latest"}'
} | jq -sc
)
echo "include=$MATRIX_INCLUDE" >> $GITHUB_OUTPUT
build-wheels:
needs: build-wheels-matrix
runs-on: ${{ matrix.os }}
name: Build ${{ matrix.only }}

strategy:
fail-fast: false
matrix:
include: ${{ fromJson(needs.build-wheels-matrix.outputs.include) }}

steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0

- name: Set up QEMU
if: runner.os == 'Linux'
uses: docker/setup-qemu-action@v2

- uses: pypa/[email protected]
timeout-minutes: 10
with:
only: ${{ matrix.only }}
env:
CIBW_BUILD_VERBOSITY: 1
CIBW_BEFORE_BUILD: 'bash -c "make install_libmagic"'
ddelange marked this conversation as resolved.
Show resolved Hide resolved

- uses: actions/upload-artifact@v3
with:
name: dist
path: wheelhouse/*.whl


publish:
needs: [build-sdist, build-wheels]
if: github.event_name == 'release'
runs-on: ubuntu-latest

steps:
- uses: actions/setup-python@v4
with:
python-version: 3.x

- uses: actions/download-artifact@v3
with:
name: dist
path: dist/

- run: ls -ltra dist/

- run: pip install -U twine python-magic --find-links ./dist

- name: Smoketest
run: python -c "import magic; magic.Magic()"

- name: Upload release assets
uses: softprops/[email protected]
with:
files: dist/*

- name: Upload to PyPI

Choose a reason for hiding this comment

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

A small improvement here might be to use the PyPa Action instead: https://github.com/pypa/gh-action-pypi-publish

The big advantage is trusted publishing, instead of storing a password or token as a secret

Copy link
Contributor Author

Choose a reason for hiding this comment

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

that's cool, thanks for sharing!

@ahupp shall I make that change and you set it up on PyPI side?

Copy link
Contributor Author

@ddelange ddelange Oct 26, 2023

Choose a reason for hiding this comment

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

Sounds like trusted publishing is the way to go. I recently got this email:

Hi ddelange!
Earlier this year, we announced that PyPI would require all users to enable a form of two-factor authentication on their accounts by the end of 2023.

Keeping your PyPI account secure is important to all of us. We encourage you to enable two-factor authentication on your PyPI account as soon as possible.

What forms of 2FA can I use?
We currently offer two main forms of 2FA for your account:

Security device including modern browsers (preferred) (e.g. Yubikey, Google Titan)
Authentication app (e.g. Google Authenticator)
Once one of these secure forms is enabled on your account, you will also need to use either Trusted Publishers (preferred) or API tokens to upload to PyPI.

What do I do if I lose my 2FA device?
As part of 2FA enrollment, you will receive one-time use recovery codes. One of them must be used to confirm receipt before 2FA is fully active. Keep these recovery codes safe - they are equivalent to your 2FA device. Should you lose access > to your 2FA device, use a recovery code to log in and swap your 2FA to a new device.

Read more aboutrecovery codes.

Why is PyPI requiring 2FA?
Keeping all users of PyPI is a shared responsibility we take seriously. Strong passwords combined with 2FA is a recognized secure practice for over a decade.

We are requiring 2FA to protect your account and the packages you upload, and to protect PyPI itself from malicious actors. The most damaging attacks are account takeover and malicious package upload.

To see this and other security events for your account, visit your account security history.

Read more on this blog post.

If you run into problems, read the FAQ page. If the solutions there are unable to resolve the issue, contact us via [email protected].

Thanks,
The PyPI Admins

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@ahupp so the last thing for you to do is adding this repo as trusted publisher to https://pypi.org/manage/project/python-magic/settings/publishing/

env:
TWINE_USERNAME: __token__
TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }}
run: |
twine upload dist/*
20 changes: 20 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
SHELL := /bin/bash

.PHONY: install_libmagic
## Install libmagic
install_libmagic:
# Debian https://packages.ubuntu.com/libmagic1
# RHEL https://git.almalinux.org/rpms/file
# Mac https://formulae.brew.sh/formula/libmagic
# Windows https://github.com/julian-r/file-windows
( ( ( 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 )
# on cibuildwheel, the lib needs to exist in the project before running setup.py
python -c "import subprocess; from magic.loader import load_lib; lib = load_lib()._name; print(f'linking {lib}'); subprocess.check_call(['cp', lib, 'magic'])"
cp /usr/share/misc/magic.mgc magic || true # only on linux
ls -ltra magic

.DEFAULT_GOAL := help
.PHONY: help
## Print Makefile documentation
help:
@perl -0 -nle 'printf("\033[36m %-15s\033[0m %s\n", "$$2", "$$1") while m/^##\s*([^\r\n]+)\n^([\w.-]+):[^=]/gm' $(MAKEFILE_LIST) | sort
37 changes: 25 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,7 @@ will fail throw if this is attempted.
```python
>>> f = magic.Magic(uncompress=True)
>>> f.from_file('testdata/test.gz')
'ASCII text (gzip compressed data, was "test", last modified: Sat Jun 28
21:32:52 2008, from Unix)'
'ASCII text (gzip compressed data, was "test", last modified: Sat Jun 28 21:32:52 2008, from Unix)'
```

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

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

### Debian/Ubuntu
### Linux

```
sudo apt-get install libmagic1
The Linux wheels should run on most systems out of the box.

Depending on your system and CPU architecture, there might be no compatible wheel uploaded. However, precompiled libmagic might still be available for your system:

```sh

Choose a reason for hiding this comment

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

It may be beneficial to add a library installation guide for SUSE as well

Copy link
Contributor Author

Choose a reason for hiding this comment

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

can you provide the relevant command?

fwiw, I think mostly all linux flavours will be covered by the wheels in the PR description, so those users won't be needing the install from source instructions provided here.

Copy link

@Privat33r-dev Privat33r-dev Jun 26, 2024

Choose a reason for hiding this comment

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

I guess that it would be
zypper install file-devel
it might as well be required to do (in vivo test is required though)
zypper install file-magic

Currently I don't have OpenSUSE at my disposal for tests and it's likely that it would be a default package. I don't promise anything, but I might find time soon-ish to test it.

# Debian/Ubuntu
apt-get update && apt-get install -y libmagic1
# Alpine
apk add --update libmagic
# RHEL
ddelange marked this conversation as resolved.
Show resolved Hide resolved
yum install file-libs
ddelange marked this conversation as resolved.
Show resolved Hide resolved
```

### Windows

You'll need DLLs for libmagic. @julian-r maintains a pypi package with the DLLs, you can fetch it with:
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.

```
pip install python-magic-bin
```
For ARM64 Windows, you'll need to compile libmagic from source.

### OSX

- When using Homebrew: `brew install libmagic`
- When using macports: `port install file`
The Mac wheels are compiled on GitHub Actions using `macos-11` runners. For older Macs, you'll need to install libmagic from source:

```sh
# homebrew
brew install libmagic
# macports
port install file
```

### Troubleshooting

Expand Down
7 changes: 6 additions & 1 deletion magic/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -330,7 +330,12 @@ def magic_descriptor(cookie, fd):


def magic_load(cookie, filename):
return _magic_load(cookie, coerce_filename(filename))
try:
return _magic_load(cookie, coerce_filename(filename))
except MagicException:
# wheels package the mime database in this directory
filename = os.path.join(os.path.dirname(__file__), 'magic.mgc')
return _magic_load(cookie, coerce_filename(filename))


magic_setflags = libmagic.magic_setflags
Expand Down
73 changes: 42 additions & 31 deletions magic/loader.py
Original file line number Diff line number Diff line change
@@ -1,50 +1,61 @@
from ctypes.util import find_library
import ctypes
import sys
import glob
import os.path
import subprocess
import sys

def _lib_candidates():
here = os.path.dirname(__file__)

yield find_library('magic')
if sys.platform == 'darwin':

if sys.platform == 'darwin':
paths = [
here,
os.path.abspath("."),
'/opt/local/lib',
'/usr/local/lib',
'/opt/homebrew/lib',
] + glob.glob('/usr/local/Cellar/libmagic/*/lib')

paths = [
'/opt/local/lib',
'/usr/local/lib',
'/opt/homebrew/lib',
] + glob.glob('/usr/local/Cellar/libmagic/*/lib')
for i in paths:
yield os.path.join(i, 'libmagic.dylib')

for i in paths:
yield os.path.join(i, 'libmagic.dylib')
elif sys.platform in ('win32', 'cygwin'):

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

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

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

elif sys.platform == 'linux':
# This is necessary because alpine is bad
yield 'libmagic.so.1'
yield find_library('magic')


def load_lib():

for lib in _lib_candidates():
# find_library returns None when lib not found
if lib is None:
continue
try:
return ctypes.CDLL(lib)
except OSError:
pass
else:
# It is better to raise an ImportError since we are importing magic module
raise ImportError('failed to find libmagic. Check your installation')
for lib in _lib_candidates():
# find_library returns None when lib not found
if lib is None:
continue
try:
return ctypes.CDLL(lib)
except OSError as exc:
pass
else:
# It is better to raise an ImportError since we are importing magic module
raise ImportError('failed to find libmagic. Check your installation')

29 changes: 28 additions & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,40 @@
import setuptools
import io
import os
import sys

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

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

def get_cmdclass():
"""Build a forward compatible ABI3 wheel when `setup.py bdist_wheel` is called."""
if sys.version_info[0] == 2:
return {}

try:
from wheel.bdist_wheel import bdist_wheel
except ImportError:
return {}

class bdist_wheel_abi3(bdist_wheel):
def get_tag(self):
python, abi, _ = super().get_tag()
# get the platform tag based on libmagic included in this wheel
self.root_is_pure = False
_, _, plat = super().get_tag()
return python, abi, plat

return {"bdist_wheel": bdist_wheel_abi3}

cmdclass = get_cmdclass()

setuptools.setup(
name='python-magic',
description='File type identification using libmagic',
Expand All @@ -23,8 +49,9 @@ def read(file_name):
long_description_content_type='text/markdown',
packages=['magic'],
package_data={
'magic': ['py.typed', '*.pyi', '**/*.pyi'],
'magic': ['py.typed', '*.pyi', '*.dylib*', '*.dll', '*.so*', 'magic.mgc']
},
cmdclass=cmdclass,
keywords="mime magic file",
license="MIT",
python_requires='>=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*',
Expand Down