SparkleHelper 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. sparklehelper-0.1.0/LICENSE +21 -0
  2. sparklehelper-0.1.0/MANIFEST.in +19 -0
  3. sparklehelper-0.1.0/PKG-INFO +371 -0
  4. sparklehelper-0.1.0/README.md +332 -0
  5. sparklehelper-0.1.0/pyproject.toml +72 -0
  6. sparklehelper-0.1.0/scripts/sync_native_deps.py +395 -0
  7. sparklehelper-0.1.0/setup.cfg +4 -0
  8. sparklehelper-0.1.0/setup.py +160 -0
  9. sparklehelper-0.1.0/src/SparkleHelper.egg-info/PKG-INFO +371 -0
  10. sparklehelper-0.1.0/src/SparkleHelper.egg-info/SOURCES.txt +43 -0
  11. sparklehelper-0.1.0/src/SparkleHelper.egg-info/dependency_links.txt +1 -0
  12. sparklehelper-0.1.0/src/SparkleHelper.egg-info/entry_points.txt +5 -0
  13. sparklehelper-0.1.0/src/SparkleHelper.egg-info/requires.txt +18 -0
  14. sparklehelper-0.1.0/src/SparkleHelper.egg-info/top_level.txt +1 -0
  15. sparklehelper-0.1.0/src/sparklehelper/__init__.py +62 -0
  16. sparklehelper-0.1.0/src/sparklehelper/_backend/__init__.py +54 -0
  17. sparklehelper-0.1.0/src/sparklehelper/_backend/_macos/__init__.py +45 -0
  18. sparklehelper-0.1.0/src/sparklehelper/_backend/_macos/_backend.py +377 -0
  19. sparklehelper-0.1.0/src/sparklehelper/_backend/_macos/_delegates.py +656 -0
  20. sparklehelper-0.1.0/src/sparklehelper/_backend/_macos/_loading.py +202 -0
  21. sparklehelper-0.1.0/src/sparklehelper/_backend/_macos/_runtime.py +263 -0
  22. sparklehelper-0.1.0/src/sparklehelper/_backend/_windows/__init__.py +20 -0
  23. sparklehelper-0.1.0/src/sparklehelper/_backend/_windows/_backend.py +335 -0
  24. sparklehelper-0.1.0/src/sparklehelper/_backend/_windows/_bindings.py +181 -0
  25. sparklehelper-0.1.0/src/sparklehelper/_backend/_windows/_loading.py +218 -0
  26. sparklehelper-0.1.0/src/sparklehelper/_backend/base.py +330 -0
  27. sparklehelper-0.1.0/src/sparklehelper/_framework.py +956 -0
  28. sparklehelper-0.1.0/src/sparklehelper/_nuitka_plugin.py +139 -0
  29. sparklehelper-0.1.0/src/sparklehelper/_pyinstaller/__init__.py +265 -0
  30. sparklehelper-0.1.0/src/sparklehelper/_pyinstaller/hook-sparklehelper.py +124 -0
  31. sparklehelper-0.1.0/src/sparklehelper/errors.py +68 -0
  32. sparklehelper-0.1.0/src/sparklehelper/sparklehelper.nuitka-package.config.yml +31 -0
  33. sparklehelper-0.1.0/src/sparklehelper/types.py +265 -0
  34. sparklehelper-0.1.0/src/sparklehelper/updater.py +474 -0
  35. sparklehelper-0.1.0/src/sparklehelper/winsparkle/winsparkle.h +663 -0
  36. sparklehelper-0.1.0/tests/test_bridge.py +93 -0
  37. sparklehelper-0.1.0/tests/test_delegates.py +264 -0
  38. sparklehelper-0.1.0/tests/test_errors.py +43 -0
  39. sparklehelper-0.1.0/tests/test_framework.py +198 -0
  40. sparklehelper-0.1.0/tests/test_nuitka.py +30 -0
  41. sparklehelper-0.1.0/tests/test_objc.py +170 -0
  42. sparklehelper-0.1.0/tests/test_types.py +170 -0
  43. sparklehelper-0.1.0/tests/test_updater.py +539 -0
  44. sparklehelper-0.1.0/tests/test_updater_windows.py +136 -0
  45. sparklehelper-0.1.0/tests/test_windows.py +586 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 LumenMarch
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,19 @@
1
+ # Sparkle.framework 与 WinSparkle.dll 由 scripts/sync_native_deps.py 在 wheel
2
+ # 构建期生成到源码树;sdist 仅含源码,不应携带这些大体积 native 产物。
3
+ # (MANIFEST.in 只影响 sdist,不影响 wheel;wheel 由 package_data 决定。)
4
+ include LICENSE
5
+ prune src/sparklehelper/Sparkle.framework
6
+ prune src/sparklehelper/licenses
7
+ exclude src/sparklehelper/Sparkle.framework.symlinks.json
8
+ global-exclude WinSparkle.dll
9
+
10
+ # 上游源码 submodule 只用于仓库中的版本锁定与 GitHub 展示;sdist 仍保持轻量。
11
+ prune Sparkle
12
+ prune winsparkle
13
+
14
+ # sdist 必须携带同步脚本与小型源码资源,否则从 sdist 构建 wheel 会失败:
15
+ # - scripts/ 不是 Python 包,不会被自动收录,但 setup.py 构建期依赖它;
16
+ # - winsparkle.h 属于 Windows 的 package_data,在非 Windows 构建的
17
+ # sdist 中不会被自动收集,需显式包含以保证跨平台可构建。
18
+ recursive-include scripts *.py
19
+ include src/sparklehelper/winsparkle/winsparkle.h
@@ -0,0 +1,371 @@
1
+ Metadata-Version: 2.4
2
+ Name: SparkleHelper
3
+ Version: 0.1.0
4
+ Summary: Python runtime interface for Sparkle and WinSparkle app updates
5
+ Author: SparkleHelper Contributors
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/LumenMarch/SparkleHelper
8
+ Project-URL: Repository, https://github.com/LumenMarch/SparkleHelper
9
+ Keywords: sparkle,winsparkle,macos,windows,auto-update,pyobjc,software-update
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Environment :: MacOS X
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Operating System :: MacOS :: MacOS X
14
+ Classifier: Operating System :: Microsoft :: Windows
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3 :: Only
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Programming Language :: Python :: 3.14
21
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
22
+ Requires-Python: >=3.11
23
+ Description-Content-Type: text/markdown
24
+ License-File: LICENSE
25
+ Requires-Dist: pyobjc-core>=10.0; sys_platform == "darwin"
26
+ Requires-Dist: pyobjc-framework-Cocoa>=10.0; sys_platform == "darwin"
27
+ Provides-Extra: dev
28
+ Requires-Dist: pytest>=7.0; extra == "dev"
29
+ Requires-Dist: pytest-cov>=4.0; extra == "dev"
30
+ Provides-Extra: demo
31
+ Requires-Dist: wxPython>=4.2; extra == "demo"
32
+ Requires-Dist: pyinstaller>=6.21.0; extra == "demo"
33
+ Requires-Dist: nuitka>=4.1.2; extra == "demo"
34
+ Provides-Extra: demo-qt
35
+ Requires-Dist: PySide6>=6.7; extra == "demo-qt"
36
+ Requires-Dist: pyinstaller>=6.21.0; extra == "demo-qt"
37
+ Requires-Dist: nuitka>=4.1.2; extra == "demo-qt"
38
+ Dynamic: license-file
39
+
40
+ # 1. SparkleHelper
41
+
42
+ [English](README.md) | [简体中文](README.zh-CN.md)
43
+
44
+ > A Python runtime interface for native app updates with
45
+ > [Sparkle](https://github.com/sparkle-project/Sparkle) on macOS and
46
+ > [WinSparkle](https://github.com/vslavik/winsparkle) on Windows.
47
+
48
+ SparkleHelper lets you integrate check-for-updates, background download, and
49
+ native update UI into packaged Python desktop apps — Sparkle for macOS `.app`
50
+ bundles and WinSparkle for Windows executables — without writing
51
+ Objective-C / Swift or Win32 update plumbing.
52
+
53
+ ## 2. Why
54
+
55
+ [Sparkle](https://github.com/sparkle-project/Sparkle) and
56
+ [WinSparkle](https://github.com/vslavik/winsparkle) are mature native update
57
+ frameworks, but they expose platform-specific Objective-C and C APIs.
58
+ SparkleHelper gives Python desktop apps one runtime facade while preserving
59
+ the native updater on each platform. It solves three things:
60
+
61
+ 1. **Runtime loading** — dynamically load the bundled `Sparkle.framework` on
62
+ macOS or `WinSparkle.dll` on Windows.
63
+ 2. **Pythonic API** — expose a typed `Updater` facade over
64
+ `SPUStandardUpdaterController` / `SPUUpdater` and the WinSparkle C API.
65
+ 3. **Offline packaging** — ship platform-native update runtimes inside wheels
66
+ and let supported bundlers collect them without network access.
67
+
68
+ ## 3. Quick start
69
+
70
+ ### 3.1. Install
71
+
72
+ ```bash
73
+ # Install this repo in editable mode, including dev dependencies
74
+ uv pip install -e ".[dev]"
75
+ ```
76
+
77
+ ### 3.2. Use in an app
78
+
79
+ On macOS, Sparkle must run inside a packaged `.app` bundle because it depends
80
+ on the bundle structure and `Info.plist`. On Windows, WinSparkle is configured
81
+ from `Updater(...)` before its native runtime is started.
82
+
83
+ macOS minimal usage:
84
+
85
+ ```python
86
+ from sparklehelper import Updater
87
+
88
+ updater = Updater() # reads SUFeedURL from Info.plist
89
+ updater.check_for_updates() # pops the native Sparkle update window
90
+ ```
91
+
92
+ Windows minimal usage:
93
+
94
+ ```python
95
+ from sparklehelper import Updater
96
+
97
+ updater = Updater(
98
+ feed_url="https://example.com/appcast.xml",
99
+ public_key="...",
100
+ company="Example",
101
+ app_name="ExampleApp",
102
+ version="0.1.0",
103
+ )
104
+ updater.check_for_updates() # pops the native WinSparkle update window
105
+ ```
106
+
107
+ On macOS, bind it to a GUI menu item and drive its enabled state via KVO:
108
+
109
+ ```python
110
+ updater = Updater()
111
+
112
+ with updater.observe_can_check_for_updates(menu_item.setEnabled):
113
+ # the menu item's state refreshes automatically while inside the block
114
+ ...
115
+ ```
116
+
117
+ Other macOS KVO properties can be subscribed to through the unified entry point:
118
+
119
+ ```python
120
+ subscription = updater.observe(
121
+ "automatically_downloads_updates",
122
+ settings_view.set_auto_download_enabled,
123
+ )
124
+ ```
125
+
126
+ macOS custom feed source and channel filtering (implement **any subset** of
127
+ `UpdaterDelegate`). On Windows, pass `feed_url` directly to `Updater(...)`:
128
+
129
+ ```python
130
+ class MyDelegate:
131
+ def feed_url_string_for_updater(self):
132
+ return "https://example.com/appcast.xml"
133
+ def allowed_channels_for_updater(self):
134
+ return ("beta",) if is_beta_user() else ()
135
+
136
+ updater = Updater(delegate=MyDelegate())
137
+ ```
138
+
139
+ ## 4. Platform configuration
140
+
141
+ ### 4.1. macOS
142
+
143
+ Sparkle's core macOS configuration lives in the `.app` `Info.plist`:
144
+
145
+ | Key | Required | Description |
146
+ |---|---|---|
147
+ | `SUFeedURL` | Yes | Public URL of appcast.xml |
148
+ | `SUPublicEDKey` | Yes | EdDSA public key used to verify update signatures |
149
+ | `SUEnableAutomaticChecks` | No | Enable automatic checks (on by default) |
150
+ | `SUScheduledCheckInterval` | No | Check interval in seconds (default 86400) |
151
+
152
+ The Nuitka wrapper supports Sparkle's full host-app plist key set exposed by
153
+ Sparkle 2.x `SUConstants.h`; pass any of them with `--sparkle-key KEY=VALUE`
154
+ or the generated kebab-case option such as `--su-automatically-update true`.
155
+
156
+ ### 4.2. Windows
157
+
158
+ WinSparkle must receive its init-time settings from Python before
159
+ `win_sparkle_init()`:
160
+
161
+ | `Updater(...)` argument | Required | Description |
162
+ |---|---|---|
163
+ | `feed_url` | Yes | Public URL of appcast.xml |
164
+ | `public_key` | Recommended | EdDSA public key used to verify update signatures |
165
+ | `company` / `app_name` / `version` | Recommended | App identity shown by WinSparkle and used for its settings |
166
+ | `build` | No | Build version used for update comparison |
167
+
168
+ ## 5. Packaging
169
+
170
+ SparkleHelper ships first-class support for the two mainstream Python
171
+ bundlers. Wheels include the native runtime for their target platform:
172
+ `Sparkle.framework` for macOS and `WinSparkle.dll` for Windows, so packaging
173
+ never downloads native assets.
174
+
175
+ ### 5.1. PyInstaller
176
+
177
+ SparkleHelper registers a built-in PyInstaller hook (via the
178
+ `pyinstaller40` entry point) that collects the platform-native updater at
179
+ build time. It uses the wheel copy by default; `SPARKLEHELPER_FRAMEWORK_PATH`
180
+ on macOS and `SPARKLEHELPER_WINSPARKLE_PATH` on Windows can override the source
181
+ for development or compatibility testing.
182
+
183
+ ```bash
184
+ pyinstaller my_app.spec
185
+ ```
186
+
187
+ On macOS, inject the Sparkle keys via `info_plist={}` in your `.spec`; see
188
+ [`examples/demo_app/build.spec`](examples/demo_app/build.spec) for a
189
+ reference. On Windows, pass WinSparkle settings to `Updater(...)`.
190
+
191
+ For macOS onefile `.app` specs shaped like `BUNDLE(exe, ...)`, use the
192
+ wrapper command instead:
193
+
194
+ ```bash
195
+ uv run sparklehelper pyinstaller my_app.spec
196
+ ```
197
+
198
+ The wrapper creates a temporary patched spec that moves `Sparkle.framework`
199
+ from `Analysis` into `BUNDLE`, so it lands in `Contents/Frameworks` instead of
200
+ the onefile `_MEI...` extraction directory. Onedir specs shaped like
201
+ `BUNDLE(coll, ...)` do not need this wrapper and should keep using plain
202
+ `pyinstaller`. If a onefile `.app` spec is built with plain `pyinstaller`, the
203
+ hook stops the build and points to this wrapper command.
204
+
205
+ ### 5.2. Nuitka
206
+
207
+ SparkleHelper ships a Nuitka package config and user plugin as package data.
208
+ Nuitka does not auto-discover either third-party resource, so the
209
+ `sparklehelper nuitka` wrapper injects both before forwarding all remaining
210
+ arguments to Nuitka.
211
+
212
+ Requirements:
213
+
214
+ - Nuitka **4.1.2** or newer.
215
+ - macOS 11 or newer for Sparkle wheels, or Windows for WinSparkle wheels.
216
+
217
+ ```bash
218
+ uv run sparklehelper nuitka \
219
+ --version 0.1.0 \
220
+ --build-version 1 \
221
+ --feed-url https://example.com/appcast.xml \
222
+ --public-ed-key YOUR_EDDSA_PUBLIC_KEY_BASE64 \
223
+ --mode=app \
224
+ my_app.py
225
+ ```
226
+
227
+ The config collects the wheel's bundled copy and places it at
228
+ `Contents/Frameworks/Sparkle.framework` on macOS and at
229
+ `WinSparkle.dll` in the Windows Nuitka dist directory; the runtime locates this
230
+ directory through Nuitka's `__compiled__.containing_dir`.
231
+
232
+ On macOS, the plugin restores the framework's top-level `Autoupdate` symlink
233
+ before Nuitka signs the app. The wrapper's
234
+ `--version` option writes `CFBundleShortVersionString` and defaults to `0.1.0`
235
+ when neither `--version` nor Nuitka's native `--macos-app-version` is passed.
236
+ `--build-version` optionally writes the Sparkle-comparable `CFBundleVersion`;
237
+ when it is omitted, the wrapper derives an Apple-compatible build version from
238
+ `--version`, for example `0.1.0` becomes build version `1`. The wrapper also
239
+ accepts
240
+ Sparkle Info.plist keys via aliases such as `--feed-url`,
241
+ `--public-ed-key`, `--automatic-checks`, and `--scheduled-check-interval`, or
242
+ via repeated `--sparkle-key KEY=VALUE` entries for the full Sparkle key set. If
243
+ Nuitka's native `--macos-app-version` is forwarded without wrapper version
244
+ options, the plugin reads Nuitka's generated `CFBundleShortVersionString` from
245
+ `Info.plist` and only fills in a missing `CFBundleVersion`. If the application
246
+ also uses pypylon,
247
+ Nuitka intentionally keeps its loader-relative framework at
248
+ `Contents/Frameworks/pypylon/pylon.framework`.
249
+
250
+ To build the demo:
251
+
252
+ ```bash
253
+ cd examples/demo_app
254
+ uv run sparklehelper nuitka \
255
+ --version 0.1.0 \
256
+ --build-version 1 \
257
+ --feed-url https://example.com/appcast.xml \
258
+ --public-ed-key YOUR_EDDSA_PUBLIC_KEY_BASE64 \
259
+ --mode=app \
260
+ demo.py
261
+ ```
262
+
263
+ The wheel does not include Sparkle release authoring tools such as
264
+ `generate_keys` or `sign_update`; obtain those separately when producing
265
+ signed updates.
266
+
267
+ ## 6. API overview
268
+
269
+ ```text
270
+ Updater main entry (wraps the native platform backend)
271
+ ├─ check_for_updates() pops the native update window
272
+ ├─ check_for_updates_in_background()
273
+ ├─ start() / reset_update_cycle() / reset_update_cycle_after_short_delay()
274
+ ├─ can_check_for_updates macOS-only KVO property
275
+ ├─ feed_url / host_bundle_path / last_update_check_date / system_profile
276
+ ├─ automatically_checks_for_updates / update_check_interval
277
+ ├─ automatically_downloads_updates / allows_automatic_updates
278
+ ├─ user_agent_string / http_headers / sends_system_profile
279
+ └─ observe(property_name, cb) -> Subscription macOS-only KVO subscription
280
+
281
+ UpdaterDelegate macOS-only optional callback Protocol
282
+ ├─ updater_may_perform_update_check()
283
+ ├─ feed_url_string_for_updater()
284
+ ├─ allowed_channels_for_updater()
285
+ ├─ feed_parameters_for_updater() / allowed_system_profile_keys_for_updater()
286
+ ├─ updater_did_find_valid_update()
287
+ ├─ updater_did_not_find_update()
288
+ ├─ updater_should_proceed_with_update() / updater_user_did_make_choice()
289
+ ├─ updater_will_download_update() / updater_did_download_update()
290
+ ├─ updater_failed_to_download_update() / user_did_cancel_download()
291
+ ├─ updater_will_extract_update() / updater_did_extract_update()
292
+ ├─ updater_will_install_update() / updater_should_relaunch_application()
293
+ ├─ updater_will_schedule_update_check() / updater_will_not_schedule_update_check()
294
+ └─ updater_did_abort() / updater_did_finish_cycle()
295
+
296
+ UpdateInfo / SystemProfileEntry / UpdateCheckResult / UserUpdateState dataclass
297
+ UpdateCheckKind / UserUpdateChoice / UserUpdateStage enums
298
+
299
+ ensure_runnable() aggregate check: platform/native runtime/config
300
+ errors SparkleError exception hierarchy
301
+ ```
302
+
303
+ ## 7. Platform constraints
304
+
305
+ - **macOS main thread**: Sparkle/Cocoa APIs must be called on the main thread;
306
+ SparkleHelper asserts this at every entry point.
307
+ - **macOS bundle**: Sparkle must be packaged as a `.app`; `ensure_runnable()`
308
+ checks the bundle and plist.
309
+ - **Sparkle ships its own UI**: `check_for_updates()` pops the native window
310
+ with zero config (built-in `SPUStandardUserDriver` + the in-framework nib).
311
+ - **macOS persistence**: dynamic settings still land in Sparkle's own
312
+ NSUserDefaults, following the official advice to "not add another layer on
313
+ top of user preferences".
314
+ - **macOS GUI run loop**: Sparkle's
315
+ `startUpdater` is asynchronous and depends on the NSApp run loop to
316
+ complete; `canCheckForUpdates` only becomes `True` once the run loop is
317
+ spinning. So the host GUI must drive the NSApp run loop — **wxPython,
318
+ PyQt/PySide, PyObjC/AppKit native** all work; **tkinter does not** (it
319
+ uses a separate Tcl event loop that does not interoperate with NSApp,
320
+ leaving menu items permanently greyed out). See
321
+ [`examples/demo_app/README.md`](examples/demo_app/README.md).
322
+ - **macOS delay `start()`**: `Updater()` does not start automatic checks by
323
+ default. In GUI apps, call `start()` after entering the mainloop (the demo
324
+ does this via `wx.CallAfter`), or pass `start=True` only when the NSApp run
325
+ loop is already ready.
326
+ - **Windows cleanup**: WinSparkle starts background work in
327
+ `win_sparkle_init()`. Call `cleanup()` before application exit, or use
328
+ `Updater(...)` as a context manager.
329
+
330
+ ## 8. Development
331
+
332
+ ```bash
333
+ uv venv && uv pip install -e ".[dev]"
334
+ uv run pytest # unit tests (mock the ObjC layer; run on any platform)
335
+ ```
336
+
337
+ ### 8.1. Building wheels (maintainers)
338
+
339
+ This is a packaging repo: the main package source tree no longer ships the
340
+ large native runtime binaries (`Sparkle.framework`, `WinSparkle.dll`). They
341
+ are fetched at wheel build time.
342
+
343
+ - Normal install — published wheels already embed the native assets, so end
344
+ users need no network access or compiler.
345
+ - Upstream source refs — `Sparkle/` and `winsparkle/` are Git submodules
346
+ pinned to upstream commits for source browsing, matching the opencv-python
347
+ packaging-repo style where GitHub shows entries such as `Sparkle @ <commit>`.
348
+ - Fresh clone — use `git clone --recursive`, or run
349
+ `git submodule update --init --recursive` after cloning.
350
+ - Building a wheel — `uv build --wheel` resolves the latest upstream
351
+ Sparkle / WinSparkle release assets, verifies their SHA256, syncs the
352
+ matching upstream license files, and unpacks them into the wheel. The archive
353
+ is cached under
354
+ `build/native-cache/` and reused on subsequent builds.
355
+ - Offline build — either keep the previously generated native files in place,
356
+ or set `SPARKLEHELPER_SKIP_NATIVE_SYNC=1`, which forbids network access and
357
+ only validates the local native/license assets (the build fails with a clear
358
+ error if they are missing).
359
+ - Tracked vs. generated — upstream source references are tracked as gitlinks;
360
+ package resources only commit `src/sparklehelper/winsparkle/winsparkle.h`.
361
+ `Sparkle.framework/`, `winsparkle/*/WinSparkle.dll`, and
362
+ `src/sparklehelper/licenses/*.txt` are git-ignored and regenerated by
363
+ `scripts/sync_native_deps.py`.
364
+
365
+ ## 9. License
366
+
367
+ SparkleHelper is MIT licensed. Bundled native dependencies are redistributed
368
+ under their upstream licenses:
369
+
370
+ - `Sparkle.framework`: `sparklehelper/licenses/Sparkle-LICENSE.txt`
371
+ - `WinSparkle.dll`: `sparklehelper/licenses/WinSparkle-LICENSE.txt`