Skip to content

Commit 0b3d5b7

Browse files
authored
Provide wheels directly for every platform (#2)
* Directly build wheels for all platforms by changing tag * Remove mscl_release_assets from origin * Add mscl_release_assets/ to gitignore * Use mscl 67.0.1, make CI work, use zipfile for extraction
1 parent bfddd5d commit 0b3d5b7

File tree

46 files changed

+664
-312824
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

46 files changed

+664
-312824
lines changed

.github/workflows/build_wheels.yml

Lines changed: 14 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
name: Build wheels
1+
name: Download, extract, build, and test wheels
22

33
on:
44
pull_request:
@@ -10,19 +10,14 @@ on:
1010

1111
jobs:
1212
build-wheels:
13-
name: Build wheels
13+
name: Download, extract, build, and wheels
1414
runs-on: ${{ matrix.os }}
1515
timeout-minutes: 5
1616
strategy:
1717
fail-fast: false
1818
matrix:
19-
os: [ubuntu-latest, windows-latest]
20-
python-version: [3.9, 3.11, 3.13]
21-
exclude:
22-
- os: windows-latest
23-
python-version: 3.9
24-
- os: windows-latest
25-
python-version: 3.13
19+
os: [ubuntu-latest, windows-latest, ubuntu-24.04-arm]
20+
python-version: [3.13]
2621

2722
steps:
2823
- uses: actions/checkout@v4
@@ -39,15 +34,21 @@ jobs:
3934

4035
- name: Install the project
4136
run: uv sync
37+
38+
- name: Download and extract the mscl assets
39+
run: |
40+
uv run download_and_extract_assets.py
4241
43-
- name: Build the wheel
44-
run: uv build --wheel
42+
- name: Build the wheels
43+
run: |
44+
uv run run_build.py
4545
46-
- name: Initialize test environment
46+
# There should be only one wheel so it should work:
47+
- name: Initialize test environment and install wheel
4748
run: |
4849
uv init --no-workspace test
4950
cd test
50-
uv add ../dist/*.whl
51+
uv add "$(python -c 'import glob; print(glob.glob("../dist/*.whl")[0])')"
5152
5253
- name: Verify installation
5354
run: uv run -- python -c "from python_mscl import mscl"

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,12 @@ wheels/
1515
# Exclude local .deb and .zip files from mscl:
1616
mscl_release_assets/*.zip
1717
mscl_release_assets/*.deb
18+
mscl_release_assets/
1819

1920
# VSCode:
2021
.vscode/
2122

2223
# Exclude build downloaded mscl files:
2324
src/python_mscl/mscl.py
2425
src/python_mscl/_mscl.so
26+
src/python_mscl/mscl.pyd

README.md

Lines changed: 11 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,9 @@
22

33
Unofficial Python package for the [Microstrain Communication Library](https://github.com/LORD-MicroStrain/MSCL/tree/master).
44

5-
This library just makes it so that we can install the MSCL library using pip. Wheels are not provided. This will fetch the necessary files for your architecture and python
6-
version, and then build the wheel for you.
5+
This library just makes it so that we can install the MSCL library using pip, and directly provides the wheels!
76

8-
It is therefore recommended to use a cache for your CI or package manager, unless you're okay with the ~20MB download every time you run your CI.
7+
Only Python 3.x wheels are provided. If you need Python 2.x wheels, please open an issue.
98

109
### Installation
1110

@@ -21,11 +20,6 @@ from python_mscl import mscl
2120
# ... use the MSCL library as you normally would
2221
```
2322

24-
### Windows support:
25-
26-
The latest mscl version (v67.0.0) only has a .zip for python 3.11. It has been confirmed that it does not work on other python versions (You would get an import error). However the build itself would still go through.
27-
28-
2923
### Versioning system:
3024

3125
This repository follows the same versioning system as the MSCL library. This is reflected in the tags of this repository.
@@ -44,13 +38,17 @@ The below steps assume you have [`uv`](https://docs.astral.sh/uv/) installed.
4438

4539
1. Clone the repo and `cd` into it.
4640
2. Optional: Create a .env file and insert your GITHUB_TOKEN= to make requests to the GitHub API.
47-
3. Edit & run `uv run main.py` to fetch the latest tagged MSCL releases and extract them.
48-
4. Run `uv build`, which will build the source distribution and wheel for your python
49-
version and architecture.
41+
3. Edit & run `uv run download_and_extract_assets.py` to fetch the latest tagged MSCL releases and extract them.
42+
4. Run `uv run run_build.py`, which will build the source distribution and wheel for your python
43+
version and architecture. The wheels will be placed in the `dist/` directory.
5044

5145
Notes for me, the maintainer:
52-
5. Optional: Run `uv publish` to publish the package to PyPI. To upload to TestPyPI, uncomment lines in `pyproject.toml`, and run `uv publish --index testpypi dist/*.tar.gz`.
53-
6. Optional: To check if the package worked correctly: `uv add --index https://test.pypi.org/simple/ --index-strategy unsafe-best-match python-mscl` in a new uv project directory.
46+
5. Make sure that the constants in `constants.py` are updated, and that the MSCL repo still follows their
47+
versioning system. If not, update rest of the files accordingly.
48+
49+
6. Optional: Run `uv publish` to publish the package to PyPI. To upload to TestPyPI, uncomment lines in `pyproject.toml`, and run `uv publish --index testpypi dist/*.whl`.
50+
51+
7. Optional: To check if the package worked correctly: `uv add --index https://test.pypi.org/simple/ --index-strategy unsafe-best-match python-mscl` in a new uv project directory.
5452

5553

5654
## Issues:

build_helpers/release_downloader.py

Lines changed: 20 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
from github.GitRelease import GitRelease
99
from github.GitReleaseAsset import GitReleaseAsset
1010

11+
from constants import ASSET_DIRECTORY, ReleaseAsset
12+
1113

1214
class GithubDownloader:
1315
"""Manages downloading the Github release assets for the mscl library, along with the
@@ -18,6 +20,7 @@ def __init__(self):
1820
self.mscl_repo = "LORD-MicroStrain/MSCL"
1921
self.python_mscl_repo = "harshil21/python-mscl"
2022
self.latest_release = None
23+
self.asset_dir = Path(ASSET_DIRECTORY)
2124

2225
def get_latest_release(self) -> GitRelease:
2326
"""Returns the latest stable release for the given repo."""
@@ -33,13 +36,19 @@ def get_latest_release(self) -> GitRelease:
3336
break
3437
return self.latest_release
3538

36-
def download_release_assets(self, output_dir: str):
37-
"""Downloads the release assets for the given repo and tag."""
39+
def download_release_assets(self, only_release: ReleaseAsset | None = None) -> None:
40+
"""Downloads the release assets from the MSCL repository.
41+
42+
Args:
43+
only_release: If set, only download the release asset for the given Python version and
44+
architecture. If not set, download all the release assets.
45+
"""
3846
release = self.get_latest_release()
39-
output_path = Path(output_dir)
47+
output_path = Path(self.asset_dir)
4048
output_path.mkdir(parents=True, exist_ok=True)
4149

4250
asset: GitReleaseAsset
51+
print(f"Downloading release assets for {only_release=}")
4352
for asset in release.get_assets():
4453
# Don't download the "Documentation" or "Examples"
4554
if "Documentation" in asset.name or "Examples" in asset.name:
@@ -51,22 +60,17 @@ def download_release_assets(self, output_dir: str):
5160
if "3" not in asset.name:
5261
continue
5362

63+
# Extract the python version, arch, and platform from the only_release, if set:
64+
if only_release:
65+
if only_release.python_version not in asset.name:
66+
continue
67+
if only_release.arch not in asset.name:
68+
continue
69+
5470
self.download_asset(output_path, asset)
71+
print(f"Downloaded {asset.name}")
5572

5673
def download_asset(self, output_path: Path, asset: GitReleaseAsset) -> None:
5774
response = requests.get(asset.browser_download_url, timeout=15)
5875
asset_path = output_path / asset.name
5976
asset_path.write_bytes(response.content)
60-
61-
def download_assets_from_folder(self, tag: str, folder_name: str) -> None:
62-
"""Downloads all the files under the `folder_name` for the given tag, from the
63-
root of the repository."""
64-
65-
repo = self.github.get_repo(self.python_mscl_repo)
66-
contents = repo.get_contents(folder_name, ref=tag)
67-
68-
for content in contents:
69-
if content.type == "file":
70-
response = requests.get(content.download_url, timeout=15)
71-
file_path = Path(content.name)
72-
file_path.write_bytes(response.content)

build_helpers/release_extractor.py

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,19 @@
11
"""Extracts the .deb and .zip releases for the mscl library."""
22

33
import os
4+
import shutil
45
import subprocess
56
from pathlib import Path
7+
from zipfile import ZipFile
8+
9+
from constants import ASSET_DIRECTORY, MSCL_VERSION
610

7-
MSCL_VERSION = "v67.0.0"
8-
"""The mscl version to extract."""
911

1012
class ReleaseExtractor:
1113
"""Will extract the .deb and .zip releases for the mscl library."""
1214

1315
def __init__(self):
14-
self.asset_dir = Path("mscl_release_assets")
16+
self.asset_dir = Path(ASSET_DIRECTORY)
1517

1618
def extract_assets(self):
1719
"""Extracts the .deb and .zip releases into the same directory."""
@@ -77,18 +79,19 @@ def extract_zip(self, file: Path) -> None:
7779
# Create a directory to extract the .zip file. Syntax: mscl-<arch>-<python-ver>-<mscl-ver>
7880
parts = file.stem.split("_")
7981
arch, py_ver = parts[2], parts[3]
80-
mscl_versioned_name = f"mscl-Windows-{arch}-{py_ver}-{MSCL_VERSION}"
82+
mscl_versioned_name = f"mscl-Windows_{arch}-{py_ver}-{MSCL_VERSION}"
8183
mscl_versioned_dir = cwd / self.asset_dir / mscl_versioned_name
8284

8385
# If output directory exists, remove it:
8486
if mscl_versioned_dir.exists():
85-
os.system(f"rm -rf {mscl_versioned_dir}")
87+
shutil.rmtree(mscl_versioned_dir)
8688

8789
mscl_versioned_dir.mkdir(parents=True, exist_ok=True)
88-
file_relative = file.absolute().relative_to(mscl_versioned_dir, walk_up=True)
8990

9091
# Extract the .zip file
91-
subprocess.run(["unzip", str(file_relative)], cwd=mscl_versioned_dir, check=True) # noqa: S603, S607
92+
with ZipFile(file, "r") as zip_ref:
93+
zip_ref.extractall(mscl_versioned_dir)
94+
print("Extracted the zip file.")
9295

9396
found_mscl_py = list(mscl_versioned_dir.rglob("mscl.py"))
9497
found_mscl_pyd = list(mscl_versioned_dir.rglob("_mscl.pyd"))
@@ -106,8 +109,18 @@ def extract_zip(self, file: Path) -> None:
106109
# Delete the remaining files in mscl_versioned_dir:
107110
for f in mscl_versioned_dir.iterdir():
108111
if f.stem in (mscl_py.stem, mscl_pyd.stem):
112+
print(f"Skipping deletion of {f}")
109113
continue
110114
if f.is_dir():
111-
os.system(f"rm -rf {f}")
115+
print(f"Deleting the directory {f}")
116+
shutil.rmtree(f)
112117
else:
118+
print(f"Deleting {f}")
113119
f.unlink()
120+
121+
# Confirm that the files still exist after deleting the rest:
122+
found_mscl_py = list(mscl_versioned_dir.rglob("mscl.py"))
123+
found_mscl_pyd = list(mscl_versioned_dir.rglob("_mscl.pyd"))
124+
125+
if not found_mscl_py or not found_mscl_pyd:
126+
raise FileNotFoundError(f"Deleted mscl.py or _mscl.pyd in {mscl_versioned_dir}!")

constants.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
"""File to store common constants used in the project."""
2+
3+
from pathlib import Path
4+
from typing import NamedTuple
5+
6+
7+
# Named tuple to store the release asset information:
8+
class ReleaseAsset(NamedTuple):
9+
"""Named tuple to store the release asset information.
10+
11+
Args:
12+
python_version: The Python version of the release asset. E.g. "Python3.9".
13+
arch: The architecture of the release asset. E.g. "amd64".
14+
"""
15+
16+
python_version: str
17+
arch: str
18+
19+
20+
ASSET_DIRECTORY = Path("mscl_release_assets")
21+
"""The directory to store the downloaded release assets."""
22+
23+
24+
# Keep this the same as the one in `hatch_build.py`!
25+
MSCL_VERSION = "v67.0.1"
26+
"""The mscl version to extract from the `ASSET_DIRECTORY`. The
27+
downloader will download the latest version despite this version number."""
28+
29+
30+
MACHINE_MAPPING_TO_ARCH = {
31+
# Linux:
32+
"x86_64": "amd64",
33+
"aarch64": "arm64",
34+
"armv7l": "armhf",
35+
# Windows:
36+
"AMD64": "Windows_x64",
37+
"x86": "Windows_x86",
38+
}

download_and_extract_assets.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
"""Entry point for downloading and extracting mscl release assets."""
2+
3+
import os
4+
import platform
5+
import sys
6+
7+
from build_helpers.release_downloader import GithubDownloader, ReleaseAsset
8+
from build_helpers.release_extractor import ReleaseExtractor
9+
from constants import MACHINE_MAPPING_TO_ARCH
10+
11+
12+
def main(github_actions: bool = False) -> None:
13+
"""Entry point to fetch the latest release assets from the Github repository & extract them.
14+
15+
:param github_actions: If the script is running in a Github Actions environment. If true,
16+
the script will download and extract the release asset of only the python version and
17+
architecture and OS type detected.
18+
"""
19+
gh = GithubDownloader()
20+
if github_actions:
21+
print(f"Downloading on {sys.version_info=}, {platform.machine()=}")
22+
gh.download_release_assets(
23+
ReleaseAsset(
24+
python_version=f"Python{sys.version_info.major}.{sys.version_info.minor}",
25+
arch=MACHINE_MAPPING_TO_ARCH.get(platform.machine()),
26+
)
27+
)
28+
else:
29+
gh.download_release_assets()
30+
31+
re = ReleaseExtractor()
32+
re.extract_assets()
33+
34+
35+
if __name__ == "__main__":
36+
if os.getenv("GITHUB_ACTIONS", "false") == "true":
37+
print("Running in Github Actions environment.")
38+
main(github_actions=True)
39+
else:
40+
print("Running locally")
41+
main()

0 commit comments

Comments
 (0)