Skip to content

Commit f78a3f2

Browse files
authored
Python bindings: Fix editable install + Execute Python2.7 workflow tests (#2044)
* Python binding: - Added missing `license` field in pyproject.toml file - Fixed editable mode install and some more code cleanup in setup.py - Refreshed README.md - Replaced f-string formatter in tests with `format` method in order to be py2-compatible - Fixed typos - PEP8 fixes * GitHub Action: Install Python2.7 and run tests for re-tagged wheels on native arch runners only * Python bindings: - Use #x formatter to format hex values
1 parent 3a01515 commit f78a3f2

File tree

10 files changed

+152
-114
lines changed

10 files changed

+152
-114
lines changed

.github/workflows/build-wheels-publish.yml

+22
Original file line numberDiff line numberDiff line change
@@ -160,14 +160,36 @@ jobs:
160160
run: |
161161
python -m pip install -U pip wheel && Get-ChildItem -Path wheelhouse/ -Filter *cp38*.whl | Foreach-Object {
162162
python -m wheel tags --python-tag='py2' --abi-tag=none $_.FullName
163+
break
163164
}
165+
164166
- name: '🚧 Python 2.7 wheels re-tagging - Non-Windows'
165167
if: matrix.os != 'windows-2019'
166168
env:
167169
PIP_BREAK_SYSTEM_PACKAGES: 1
168170
run: |
169171
python3 -m pip install -U pip wheel && python3 -m wheel tags --python-tag='py2' --abi-tag=none wheelhouse/*cp38*.whl
170172
173+
- uses: LizardByte/setup-python-action@master
174+
if: (matrix.os == 'ubuntu-latest' && matrix.arch == 'x86_64' && matrix.cibw_build == 'cp38-manylinux') || matrix.os == 'macos-latest' || (matrix.os == 'windows-2019' && matrix.arch == 'AMD64')
175+
with:
176+
python-version: 2.7
177+
178+
- name: 'Python 2.7 tests - Windows'
179+
if: matrix.os == 'windows-2019' && matrix.arch == 'AMD64'
180+
run: |
181+
C:\Python27\python.exe -m pip install -U pip pytest && Get-ChildItem -Path wheelhouse/ -Filter *py2*.whl | Foreach-Object {
182+
C:\Python27\python.exe -m pip install $_.FullName
183+
C:\Python27\python.exe -m pytest bindings/python/tests
184+
break
185+
}
186+
187+
# we install and test python2.7 wheels only on native arch
188+
# NOTE: no python2.7 support for macos-13: https://github.com/LizardByte/setup-python-action/issues/2
189+
- name: 'Python 2.7 tests - Non-Windows'
190+
if: (matrix.os == 'ubuntu-latest' && matrix.arch == 'x86_64' && matrix.cibw_build == 'cp38-manylinux') || matrix.os == 'macos-latest'
191+
run: python2 -m pip install wheelhouse/*py2*.whl && python2 -m pip install -U pip pytest && python2 -m pytest bindings/python/tests
192+
171193
- uses: actions/upload-artifact@v4
172194
with:
173195
name: cibw-wheels-${{ matrix.os }}-${{ strategy.job-index }}-py38

bindings/python/README.md

+6-11
Original file line numberDiff line numberDiff line change
@@ -25,27 +25,21 @@ Originally written by Nguyen Anh Quynh, polished and redesigned by elicn, mainta
2525
Install a prebuilt wheel from PyPI:
2626

2727
```bash
28-
pip3 install unicorn
28+
python3 -m pip install unicorn
2929
```
3030

3131
In case you would like to develop the bindings:
3232

3333
```bash
34-
# Python3
35-
DEBUG=1 THREADS=4 pip3 install --user -e .
34+
DEBUG=1 THREADS=4 python3 -m pip install --user -e .
3635
# Workaround for Pylance
37-
DEBUG=1 THREADS=4 pip3 install --user -e . --config-settings editable_mode=strict
38-
# Python2
39-
DEBUG=1 THREADS=4 pip install -e .
36+
DEBUG=1 THREADS=4 python3 -m pip install --user -e . --config-settings editable_mode=strict
4037
```
4138

4239
or install it by building it by yourself:
4340

4441
```bash
45-
# Python3
46-
THREADS=4 pip3 install --user .
47-
# Python2, unfortunately `pip2` doesn't support in-tree build
48-
THREADS=4 python3 setup.py install
42+
THREADS=4 python3 -m pip install --user .
4943
```
5044

5145
Explanations for arguments:
@@ -59,4 +53,5 @@ Note that you should setup a valid building environment according to docs/COMPIL
5953

6054
## Python2 compatibility
6155

62-
By default, Unicorn python bindings will be maintained against Python3 as it offers more powerful features which improves developing efficiency. Meanwhile, Unicorn will only keep compatible with all features Unicorn1 offers regarding Python2 because Python2 has reached end-of-life for more than 3 years as the time of writing this README. While offering all features for both Python2 & Python3 is desirable and doable, it inevitably costs too much efforts to maintain and few users really rely on this. Therefore, we assume that if users still stick to Python2, previous Unicorn1 features we offer should be enough. If you really want some new features Unicorn2 offers, please check and pull request to `unicorn/unicorn_py2``. We are happy to review and accept!
56+
By default, Unicorn python bindings works with Python3.7 and above, as it offers more powerful features which improves developing efficiency compared to Python2. However, Unicorn will only keep compatible with all features Unicorn1 offers regarding Python2 because it has reached end-of-life for more than 3 years at the time of writing this README. While offering all features for both Python2 & Python3 is desirable and doable, it inevitably costs too much efforts to maintain and few users really rely on this. Therefore, we assume that if users still stick to Python2, previous Unicorn1 features should be enough. If you really want some new features Unicorn2 offers, please check and pull request to `unicorn/unicorn_py2`. We are happy to review and accept!
57+
Even though the build of wheel packages requires Python3, it's still possible to re-tag the wheel produced from Python3 with `py2` tag and then run `python2 -m pip install <retagged-wheel-py>`. For detailed commands please refer to our workflow files.

bindings/python/pyproject.toml

+1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ authors = [
1212
description = "Unicorn CPU emulator engine"
1313
readme = "README.md"
1414
keywords = ["emulation", "qemu", "unicorn"]
15+
license = { text = "BSD License" }
1516
classifiers = [
1617
'License :: OSI Approved :: BSD License',
1718
'Programming Language :: Python :: 2.7',

bindings/python/setup.py

+3-27
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,8 @@
88
import subprocess
99
import sys
1010
from setuptools import setup
11-
from setuptools.command.build import build
11+
from setuptools.command.build_py import build_py
1212
from setuptools.command.sdist import sdist
13-
from setuptools.command.bdist_egg import bdist_egg
1413

1514
log = logging.getLogger(__name__)
1615

@@ -134,7 +133,7 @@ def run(self):
134133
return super().run()
135134

136135

137-
class CustomBuild(build):
136+
class CustomBuild(build_py):
138137
def run(self):
139138
if 'LIBUNICORN_PATH' in os.environ:
140139
log.info("Skipping building C extensions since LIBUNICORN_PATH is set")
@@ -144,30 +143,7 @@ def run(self):
144143
return super().run()
145144

146145

147-
class CustomBDistEgg(bdist_egg):
148-
def run(self):
149-
self.run_command('build')
150-
return super().run()
151-
152-
153-
cmdclass = {'build': CustomBuild, 'sdist': CustomSDist, 'bdist_egg': CustomBDistEgg}
154-
155-
try:
156-
from setuptools.command.develop import develop
157-
158-
159-
class CustomDevelop(develop):
160-
def run(self):
161-
log.info("Building C extensions")
162-
build_libraries()
163-
return super().run()
164-
165-
166-
cmdclass['develop'] = CustomDevelop
167-
except ImportError:
168-
print("Proper 'develop' support unavailable.")
169-
170146
setup(
171-
cmdclass=cmdclass,
147+
cmdclass={'build_py': CustomBuild, 'sdist': CustomSDist},
172148
has_ext_modules=lambda: True, # It's not a Pure Python wheel
173149
)

bindings/python/tests/test_arm64.py

+3-2
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,8 @@ def test_arm64_read_sctlr():
8787

8888
def test_arm64_hook_mrs():
8989
def _hook_mrs(uc, reg, cp_reg, _):
90-
print(f">>> Hook MRS instruction: reg = 0x{reg:x}(UC_ARM64_REG_X2) cp_reg = {cp_reg}")
90+
print(">>> Hook MRS instruction: reg = {reg:#x}(UC_ARM64_REG_X2) cp_reg = {cp_reg}".format(reg=reg,
91+
cp_reg=cp_reg))
9192
uc.reg_write(reg, 0x114514)
9293
print(">>> Write 0x114514 to X")
9394

@@ -111,7 +112,7 @@ def _hook_mrs(uc, reg, cp_reg, _):
111112
# Start emulation
112113
mu.emu_start(0x1000, 0x1000 + len(ARM64_MRS_CODE))
113114

114-
print(f">>> X2 = {mu.reg_read(UC_ARM64_REG_X2):x}")
115+
print(">>> X2 = {reg:#x}".format(reg=mu.reg_read(UC_ARM64_REG_X2)))
115116

116117
except UcError as e:
117118
print("ERROR: %s" % e)

bindings/python/tests/test_ctl.py

+18-7
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
# Sample code for Unicorn.
33
# By Lazymio(@wtdcode), 2021
44

5+
import pytest
6+
import sys
57
from unicorn import *
68
from unicorn.x86_const import *
79
from datetime import datetime
@@ -20,7 +22,9 @@ def test_uc_ctl_read():
2022

2123
timeout = uc.ctl_get_timeout()
2224

23-
print(f">>> arch={arch} mode={mode} page size={page_size} timeout={timeout}")
25+
print(">>> arch={arch} mode={mode} page size={page_size} timeout={timeout}".format(arch=arch, mode=mode,
26+
page_size=page_size,
27+
timeout=timeout))
2428

2529

2630
def time_emulation(uc, start, end):
@@ -31,6 +35,8 @@ def time_emulation(uc, start, end):
3135
return (datetime.now() - n).total_seconds() * 1e6
3236

3337

38+
# TODO: Check if worth adapting the ctl_request_cache method for py2 bindings
39+
@pytest.mark.skipif(sys.version_info < (3, 7), reason="requires python3.7 or higher")
3440
def test_uc_ctl_tb_cache():
3541
# Initialize emulator in X86-32bit mode
3642
uc = Uc(UC_ARCH_X86, UC_MODE_32)
@@ -52,7 +58,7 @@ def test_uc_ctl_tb_cache():
5258
# Now we request cache for all TBs.
5359
for i in range(8):
5460
tb = uc.ctl_request_cache(addr + i * 512)
55-
print(f">>> TB is cached at {hex(tb[0])} which has {tb[1]} instructions with {tb[2]} bytes")
61+
print(">>> TB is cached at {:#x} which has {} instructions with {} bytes".format(tb[0], tb[1], tb[2]))
5662

5763
# Do emulation with all TB cached.
5864
cached = time_emulation(uc, addr, addr + len(code))
@@ -63,17 +69,22 @@ def test_uc_ctl_tb_cache():
6369

6470
evicted = time_emulation(uc, addr, addr + len(code))
6571

66-
print(f">>> Run time: First time {standard}, Cached: {cached}, Cached evicted: {evicted}")
72+
print(">>> Run time: First time {standard}, Cached: {cached}, Cached evicted: {evicted}".format(standard=standard,
73+
cached=cached,
74+
evicted=evicted))
6775

6876

6977
def trace_new_edge(uc, cur, prev, data):
70-
print(f">>> Getting a new edge from {hex(prev.pc + prev.size - 1)} to {hex(cur.pc)}")
78+
print(">>> Getting a new edge from {:#x} to {:#x}".format(prev.pc + prev.size - 1, cur.pc))
7179

7280

7381
def trace_tcg_sub(uc, address, arg1, arg2, size, data):
74-
print(f">>> Get a tcg sub opcode at {hex(address)} with args: {arg1} and {arg2}")
82+
print(">>> Get a tcg sub opcode at {address:#x} with args: {arg1} and {arg2}".format(address=address, arg1=arg1,
83+
arg2=arg2))
7584

7685

86+
# TODO: Check if worth adapting the hook_add method for py2 bindings
87+
@pytest.mark.skipif(sys.version_info < (3, 7), reason="requires python3.7 or higher")
7788
def test_uc_ctl_exits():
7889
uc = Uc(UC_ARCH_X86, UC_MODE_32)
7990
addr = 0x1000
@@ -110,15 +121,15 @@ def test_uc_ctl_exits():
110121
eax = uc.reg_read(UC_X86_REG_EAX)
111122
ebx = uc.reg_read(UC_X86_REG_EBX)
112123

113-
print(f">>> eax = {hex(eax)} and ebx = {hex(ebx)} after the first emulation")
124+
print(">>> eax = {eax:#x} and ebx = {ebx:#x} after the first emulation".format(eax=eax, ebx=ebx))
114125

115126
# This should stop at ADDRESS + 8, even though we don't provide an exit.
116127
uc.emu_start(addr, 0)
117128

118129
eax = uc.reg_read(UC_X86_REG_EAX)
119130
ebx = uc.reg_read(UC_X86_REG_EBX)
120131

121-
print(f">>> eax = {hex(eax)} and ebx = {hex(ebx)} after the first emulation")
132+
print(">>> eax = {eax:#x} and ebx = {ebx:#x} after the first emulation".format(eax=eax, ebx=ebx))
122133

123134

124135
if __name__ == "__main__":

bindings/python/tests/test_network_auditing.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -356,7 +356,7 @@ def hook_intr(uc, intno, user_data):
356356
fd = args[0]
357357
how = args[1]
358358

359-
msg = "fd(%d) is shutted down because of %d" % (fd, how)
359+
msg = "fd(%d) is shut down because of %d" % (fd, how)
360360
fd_chains.add_log(fd, msg)
361361
print_sockcall(msg)
362362

bindings/python/tests/test_x86.py

+6-3
Original file line numberDiff line numberDiff line change
@@ -639,13 +639,16 @@ def test_x86_16():
639639

640640

641641
def mmio_read_cb(uc, offset, size, data):
642-
print(f">>> Read IO memory at offset {hex(offset)} with {hex(size)} bytes and return 0x19260817")
642+
print(">>> Read IO memory at offset {offset:#x} with {size:#x} bytes and return 0x19260817".format(offset=offset,
643+
size=size))
643644

644645
return 0x19260817
645646

646647

647648
def mmio_write_cb(uc, offset, size, value, data):
648-
print(f">>> Write value {hex(value)} to IO memory at offset {hex(offset)} with {hex(size)} bytes")
649+
print(">>> Write value {value:#x} to IO memory at offset {offset:#x} with {size:#x} bytes".format(value=value,
650+
offset=offset,
651+
size=size))
649652

650653

651654
def test_i386_mmio():
@@ -668,7 +671,7 @@ def test_i386_mmio():
668671
mu.emu_start(0x10000, 0x10000 + len(X86_MMIO_CODE))
669672

670673
# now print out some registers
671-
print(f">>> Emulation done. ECX={hex(mu.reg_read(UC_X86_REG_ECX))}")
674+
print(">>> Emulation done. ECX={reg:#x}".format(reg=mu.reg_read(UC_X86_REG_ECX)))
672675

673676
except UcError as e:
674677
print("ERROR: %s" % e)

0 commit comments

Comments
 (0)