Skip to content

Commit 6bc3849

Browse files
authored
Merge pull request #63 from psobot/psobot/multiversion
Add internal mechanisms for backwards compatibility.
2 parents 2accc20 + f790baf commit 6bc3849

Some content is hidden

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

57 files changed

+1208
-1133
lines changed

.coveragerc

Lines changed: 0 additions & 8 deletions
This file was deleted.

.github/workflows/python-package.yml

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ jobs:
1616
runs-on: ubuntu-latest
1717
strategy:
1818
matrix:
19-
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"]
19+
python-version: ["3.10", "3.11", "3.12", "3.13"]
2020
steps:
2121
- uses: actions/checkout@v2
2222
- name: Set up Python ${{ matrix.python-version }}
@@ -32,13 +32,13 @@ jobs:
3232
echo "::set-env name=PATH::$PATH:$PWD/protoc/bin/"
3333
sudo apt-get install -qq libsnappy-dev
3434
python -m pip install --upgrade pip
35-
pip install ruff pytest
35+
pip install ruff pytest rich 'protobuf>=3.20.0rc1,<4'
3636
pip install -r requirements.txt
3737
env:
3838
ACTIONS_ALLOW_UNSECURE_COMMANDS: 'true'
39-
- name: Build package
40-
run: make
39+
- name: Build gencode
40+
run: PYTHONPATH=$PYTHONPATH:$(pwd) python3 dumper/run.py
4141
- name: Lint with ruff
42-
run: ruff check . --exclude keynote_parser/generated
42+
run: ruff check .
4343
- name: Test with pytest
44-
run: PYTHONPATH=$PYTHONPATH:$(pwd) pytest --cov=keynote_parser
44+
run: PYTHONPATH=$PYTHONPATH:$(pwd) pytest

.github/workflows/python-publish.yml

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,12 +28,15 @@ jobs:
2828
sudo apt-get install -qq libsnappy-dev
2929
pip install -r requirements.txt
3030
python -m pip install --upgrade pip
31-
pip install setuptools wheel twine
31+
pip install setuptools wheel twine build rich 'protobuf>=3.20.0rc1,<4'
3232
- name: Build and publish
3333
env:
3434
TWINE_USERNAME: __token__
3535
TWINE_PASSWORD: ${{ secrets.PYPI_TOKEN }}
3636
run: |
37-
make
38-
python setup.py sdist bdist_wheel
37+
# Build the gencode:
38+
PYTHONPATH=$PYTHONPATH:$(pwd) python3 dumper/run.py
39+
# Build the package:
40+
python -m build
41+
# Upload the package:
3942
twine upload dist/*

.gitignore

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
__pycache__/
55
*.py[cod]
66
*$py.class
7-
keynote_parser/generated/
7+
keynote_parser/versions/v*/generated/
88

99
# C extensions
1010
*.so
@@ -115,4 +115,6 @@ venv.bak/
115115
dmypy.json
116116

117117
# Pyre type checker
118-
.pyre/
118+
.pyre/
119+
120+
uv.lock

Makefile

Lines changed: 0 additions & 30 deletions
This file was deleted.

dumper/Makefile

Lines changed: 0 additions & 53 deletions
This file was deleted.

dumper/__init__.py

Whitespace-only changes.

dumper/extract_mapping.py

Lines changed: 65 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,11 @@
1111
import argparse
1212
import enum
1313
import json
14+
import logging
1415
import os
16+
import sys
1517
import time
1618

17-
import lldb
18-
1919

2020
class StateType(enum.Enum):
2121
Invalid = 0
@@ -26,6 +26,10 @@ class StateType(enum.Enum):
2626
Stopped = 5
2727
Running = 6
2828
Stepping = 7
29+
Crashed = 8
30+
Detached = 9
31+
Exited = 10
32+
Suspended = 11
2933

3034

3135
class StopReason(enum.Enum):
@@ -59,43 +63,85 @@ def main():
5963
args = parser.parse_args()
6064

6165
if os.path.exists(args.output):
62-
print(f"Removing output file {args.output}...")
66+
logging.info(f"Removing output file {args.output}...")
6367
os.remove(args.output)
6468

65-
print("Creating debugger...")
69+
mapping = extract_mapping(args.exe)
70+
with open(args.output, "w") as f:
71+
json.dump(mapping, f, indent=2)
72+
73+
74+
def extract_mapping(exe: str) -> dict[int, str]:
75+
# Add the installed LLVM Python path to the Python path and error if the Python version does not match:
76+
# i.e.: /opt/homebrew/opt/llvm/libexec/python3.13/site-packages
77+
LLVM_PYTHON_ROOT = "/opt/homebrew/opt/llvm/libexec"
78+
if not os.path.exists(LLVM_PYTHON_ROOT):
79+
raise ImportError(
80+
f"{LLVM_PYTHON_ROOT} does not exist. Please install LLVM/LLDB first."
81+
)
82+
83+
existing_versions = [
84+
x for x in os.listdir(LLVM_PYTHON_ROOT) if x.startswith("python")
85+
]
86+
87+
THIS_PYTHON_LLVM_PATH = f"{LLVM_PYTHON_ROOT}/python{sys.version_info.major}.{sys.version_info.minor}/site-packages"
88+
if not os.path.exists(THIS_PYTHON_LLVM_PATH):
89+
raise ImportError(
90+
"Your system has LLVM/LLDB installed, but it is not the same version as the Python interpreter "
91+
f"you are using; found: {', '.join(existing_versions)}, but the current Python version is "
92+
f"{sys.version_info.major}.{sys.version_info.minor}. Please install the same "
93+
"version of LLVM/LLDB as your Python interpreter."
94+
)
95+
96+
sys.path.append(THIS_PYTHON_LLVM_PATH)
97+
98+
import lldb
99+
100+
logging.info("Creating debugger...")
66101
debugger = lldb.SBDebugger.Create()
67102
debugger.SetAsync(False)
68-
print(f"Creating target of {args.exe}...")
69-
target = debugger.CreateTargetWithFileAndArch(args.exe, None)
70-
print("Setting breakpoint for _sendFinishLaunchingNotification...")
103+
logging.info(f"Creating target of {exe}...")
104+
target = debugger.CreateTargetWithFileAndArch(exe, None)
105+
logging.info("Setting breakpoint for _sendFinishLaunchingNotification...")
71106
target.BreakpointCreateByName("_sendFinishLaunchingNotification")
72107

73-
print("Setting breakpoint for _handleAEOpenEvent:...")
108+
logging.info("Setting breakpoint for _handleAEOpenEvent:...")
74109
target.BreakpointCreateByName("_handleAEOpenEvent:")
75110

76-
print("Setting breakpoint for [CKContainer containerWithIdentifier:]...")
111+
logging.info("Setting breakpoint for [CKContainer containerWithIdentifier:]...")
77112
# let's break in the CloudKit code and early exit the function before it can raise an exception:
78113
target.BreakpointCreateByName("[CKContainer containerWithIdentifier:]")
79114

80-
print("Setting breakpoint for ___lldb_unnamed_symbol[0-9]+...")
115+
logging.info("Setting breakpoint for ___lldb_unnamed_symbol[0-9]+...")
81116
# In later Keynote versions, 'containerWithIdentifier' isn't called directly, but we can break on similar methods:
82117
# Note: this __lldb_unnamed_symbol hack was determined by painstaking experimentation. It will break again for sure.
83118
target.BreakpointCreateByRegex("___lldb_unnamed_symbol[0-9]+", "CloudKit")
84119

85-
print("Launching process...")
120+
logging.info("Launching process...")
86121
process = target.LaunchSimple(None, None, os.getcwd())
87122

88123
if not process:
89-
raise ValueError("Failed to launch process: " + args.exe)
124+
raise ValueError(f"Failed to launch process: {exe}")
90125
try:
91-
print("Waiting for process to stop on a breakpoint...")
92-
while process.GetState() != lldb.eStateStopped:
93-
print(f"Current state: {StateType(process.GetState())}")
126+
logging.info("Waiting for process to stop on a breakpoint...")
127+
while (
128+
process.GetState() != lldb.eStateStopped
129+
and process.GetState() != lldb.eStateExited
130+
):
131+
logging.info(f"Current state: {StateType(process.GetState())}")
94132
time.sleep(0.1)
95133

134+
if process.GetState() == lldb.eStateExited:
135+
raise ValueError(
136+
"Process exited before stopping on a breakpoint. "
137+
"Ensure the process is properly code signed."
138+
)
139+
96140
while process.GetState() == lldb.eStateStopped:
97141
thread = process.GetThreadAtIndex(0)
98-
print(f"Thread: {thread} stopped at: {StopReason(thread.GetStopReason())}")
142+
logging.info(
143+
f"Thread: {thread} stopped at: {StopReason(thread.GetStopReason())}"
144+
)
99145
match thread.GetStopReason():
100146
case lldb.eStopReasonBreakpoint:
101147
if any(
@@ -113,7 +159,7 @@ def main():
113159
else:
114160
break
115161
case lldb.eStopReasonException:
116-
print(repr(thread) + "\n")
162+
logging.info(repr(thread) + "\n")
117163
raise NotImplementedError(
118164
f"LLDB caught exception, {__file__} needs to be updated to handle."
119165
)
@@ -134,11 +180,8 @@ def main():
134180
if x.strip()
135181
]
136182
mapping = [(int(a), b.split(" ")[-1]) for a, b in split if "null" not in b]
137-
print(f"Extracted mapping with {len(mapping):,} elements.")
138-
results = json.dumps(dict(sorted(mapping)), indent=2)
139-
with open(args.output, "w") as f:
140-
f.write(results)
141-
print(f"Wrote {len(results):,} bytes of mapping to {args.output}.")
183+
logging.info(f"Extracted mapping with {len(mapping):,} elements.")
184+
return dict(sorted(mapping))
142185
finally:
143186
process.Kill()
144187

0 commit comments

Comments
 (0)