pythonnative 0.40.0__cp313-cp313-win_amd64.whl
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.
- pythonnative/__init__.py +411 -0
- pythonnative/_ios_log.py +92 -0
- pythonnative/_yoga.cp313-win_amd64.pyd +0 -0
- pythonnative/alerts.py +236 -0
- pythonnative/animated.py +1687 -0
- pythonnative/animation_graph.py +78 -0
- pythonnative/appearance.py +124 -0
- pythonnative/bootstrap.py +100 -0
- pythonnative/bridge/__init__.py +302 -0
- pythonnative/bridge/android.py +78 -0
- pythonnative/bridge/codec.py +170 -0
- pythonnative/bridge/commits.py +114 -0
- pythonnative/bridge/fake.py +243 -0
- pythonnative/bridge/ios.py +138 -0
- pythonnative/bridge/web.py +489 -0
- pythonnative/cli/__init__.py +7 -0
- pythonnative/cli/pn.py +1214 -0
- pythonnative/component.py +271 -0
- pythonnative/components/__init__.py +111 -0
- pythonnative/components/_base.py +166 -0
- pythonnative/components/controls.py +521 -0
- pythonnative/components/layout.py +518 -0
- pythonnative/components/lists.py +785 -0
- pythonnative/components/media.py +204 -0
- pythonnative/components/overlays.py +107 -0
- pythonnative/components/pressable.py +252 -0
- pythonnative/components/structural.py +167 -0
- pythonnative/components/text.py +291 -0
- pythonnative/devclient.py +824 -0
- pythonnative/devserver/__init__.py +34 -0
- pythonnative/devserver/server.py +718 -0
- pythonnative/devserver/static/animation_graph.js +82 -0
- pythonnative/devserver/static/bridge.js +147 -0
- pythonnative/devserver/static/colors.js +70 -0
- pythonnative/devserver/static/host.js +552 -0
- pythonnative/devserver/static/index.html +60 -0
- pythonnative/devserver/static/layout.js +85 -0
- pythonnative/devserver/static/preview.css +415 -0
- pythonnative/devserver/static/renderer.js +1994 -0
- pythonnative/devserver/static/shell.js +345 -0
- pythonnative/devserver/static/yoga/LICENSE +21 -0
- pythonnative/devserver/static/yoga/binaries/yoga-wasm-base64-esm.js +75 -0
- pythonnative/devserver/static/yoga/src/generated/YGEnums.d.ts +189 -0
- pythonnative/devserver/static/yoga/src/generated/YGEnums.js +211 -0
- pythonnative/devserver/static/yoga/src/generated/YGEnums.js.map +1 -0
- pythonnative/devserver/static/yoga/src/index.d.ts +12 -0
- pythonnative/devserver/static/yoga/src/index.js +16 -0
- pythonnative/devserver/static/yoga/src/index.js.map +1 -0
- pythonnative/devserver/static/yoga/src/load.d.ts +11 -0
- pythonnative/devserver/static/yoga/src/load.js +17 -0
- pythonnative/devserver/static/yoga/src/load.js.map +1 -0
- pythonnative/devserver/static/yoga/src/wrapAssembly.d.ts +155 -0
- pythonnative/devserver/static/yoga/src/wrapAssembly.js +125 -0
- pythonnative/devserver/static/yoga/src/wrapAssembly.js.map +1 -0
- pythonnative/devserver/watcher.py +267 -0
- pythonnative/devserver/ws.py +421 -0
- pythonnative/diagnostics.py +241 -0
- pythonnative/element.py +159 -0
- pythonnative/equality.py +19 -0
- pythonnative/events.py +231 -0
- pythonnative/gestures.py +1333 -0
- pythonnative/hooks.py +1621 -0
- pythonnative/hosts/__init__.py +63 -0
- pythonnative/hosts/base.py +596 -0
- pythonnative/hosts/native.py +302 -0
- pythonnative/hot_reload.py +585 -0
- pythonnative/layout.py +328 -0
- pythonnative/mutations.py +142 -0
- pythonnative/native/android/build.gradle +52 -0
- pythonnative/native/android/gradle.properties +3 -0
- pythonnative/native/android/settings.gradle +12 -0
- pythonnative/native/android/src/main/AndroidManifest.xml +11 -0
- pythonnative/native/android/src/main/cpp/CMakeLists.txt +9 -0
- pythonnative/native/android/src/main/cpp/yoga_jni.cpp +52 -0
- pythonnative/native/android/src/main/java/com/pythonnative/generated/NativeModules.kt +5 -0
- pythonnative/native/android/src/main/java/com/pythonnative/generated/NativeProps.kt +3705 -0
- pythonnative/native/android/src/main/java/com/pythonnative/generated/PNContracts.kt +129 -0
- pythonnative/native/android/src/main/java/com/pythonnative/runtime/PNBridge.kt +261 -0
- pythonnative/native/android/src/main/java/com/pythonnative/runtime/PythonHost.kt +20 -0
- pythonnative/native/android/src/main/java/com/pythonnative/runtime/animation/AnimationGraph.kt +108 -0
- pythonnative/native/android/src/main/java/com/pythonnative/runtime/animation/AnimationSpecs.kt +141 -0
- pythonnative/native/android/src/main/java/com/pythonnative/runtime/animation/PNAnimator.kt +257 -0
- pythonnative/native/android/src/main/java/com/pythonnative/runtime/bridge/CommitState.kt +106 -0
- pythonnative/native/android/src/main/java/com/pythonnative/runtime/bridge/JsonUtil.kt +175 -0
- pythonnative/native/android/src/main/java/com/pythonnative/runtime/bridge/MainThread.kt +40 -0
- pythonnative/native/android/src/main/java/com/pythonnative/runtime/bridge/PNLog.kt +33 -0
- pythonnative/native/android/src/main/java/com/pythonnative/runtime/bridge/PNRegistry.kt +82 -0
- pythonnative/native/android/src/main/java/com/pythonnative/runtime/bridge/PNTransaction.kt +80 -0
- pythonnative/native/android/src/main/java/com/pythonnative/runtime/bridge/TransactionApplier.kt +117 -0
- pythonnative/native/android/src/main/java/com/pythonnative/runtime/bridge/ViewRegistry.kt +74 -0
- pythonnative/native/android/src/main/java/com/pythonnative/runtime/components/BuiltinComponents.kt +37 -0
- pythonnative/native/android/src/main/java/com/pythonnative/runtime/components/ButtonManager.kt +32 -0
- pythonnative/native/android/src/main/java/com/pythonnative/runtime/components/ComponentManager.kt +213 -0
- pythonnative/native/android/src/main/java/com/pythonnative/runtime/components/ContainerManagers.kt +69 -0
- pythonnative/native/android/src/main/java/com/pythonnative/runtime/components/ControlManagers.kt +186 -0
- pythonnative/native/android/src/main/java/com/pythonnative/runtime/components/DatePickerManager.kt +123 -0
- pythonnative/native/android/src/main/java/com/pythonnative/runtime/components/ImageLoader.kt +141 -0
- pythonnative/native/android/src/main/java/com/pythonnative/runtime/components/ImageManager.kt +162 -0
- pythonnative/native/android/src/main/java/com/pythonnative/runtime/components/ModalManager.kt +134 -0
- pythonnative/native/android/src/main/java/com/pythonnative/runtime/components/PNColor.kt +201 -0
- pythonnative/native/android/src/main/java/com/pythonnative/runtime/components/PickerManager.kt +86 -0
- pythonnative/native/android/src/main/java/com/pythonnative/runtime/components/PortalManager.kt +55 -0
- pythonnative/native/android/src/main/java/com/pythonnative/runtime/components/PressableManager.kt +122 -0
- pythonnative/native/android/src/main/java/com/pythonnative/runtime/components/ScreenStackManager.kt +110 -0
- pythonnative/native/android/src/main/java/com/pythonnative/runtime/components/ScrollViewManager.kt +214 -0
- pythonnative/native/android/src/main/java/com/pythonnative/runtime/components/SegmentedControlManager.kt +113 -0
- pythonnative/native/android/src/main/java/com/pythonnative/runtime/components/StatusBarManager.kt +52 -0
- pythonnative/native/android/src/main/java/com/pythonnative/runtime/components/TabBarManager.kt +131 -0
- pythonnative/native/android/src/main/java/com/pythonnative/runtime/components/TextInputManager.kt +250 -0
- pythonnative/native/android/src/main/java/com/pythonnative/runtime/components/TextManager.kt +233 -0
- pythonnative/native/android/src/main/java/com/pythonnative/runtime/components/ViewStyler.kt +355 -0
- pythonnative/native/android/src/main/java/com/pythonnative/runtime/components/VirtualListManager.kt +159 -0
- pythonnative/native/android/src/main/java/com/pythonnative/runtime/components/WebViewManager.kt +149 -0
- pythonnative/native/android/src/main/java/com/pythonnative/runtime/gestures/GestureArbiter.kt +233 -0
- pythonnative/native/android/src/main/java/com/pythonnative/runtime/gestures/GestureCoordinator.kt +127 -0
- pythonnative/native/android/src/main/java/com/pythonnative/runtime/gestures/GestureRecognizers.kt +527 -0
- pythonnative/native/android/src/main/java/com/pythonnative/runtime/layout/NativeLayout.kt +145 -0
- pythonnative/native/android/src/main/java/com/pythonnative/runtime/layout/YogaNode.kt +29 -0
- pythonnative/native/android/src/main/java/com/pythonnative/runtime/modules/BuiltinModules.kt +73 -0
- pythonnative/native/android/src/main/java/com/pythonnative/runtime/modules/DeviceModules.kt +316 -0
- pythonnative/native/android/src/main/java/com/pythonnative/runtime/modules/HostModule.kt +61 -0
- pythonnative/native/android/src/main/java/com/pythonnative/runtime/modules/MediaModules.kt +200 -0
- pythonnative/native/android/src/main/java/com/pythonnative/runtime/modules/NativeModule.kt +158 -0
- pythonnative/native/android/src/main/java/com/pythonnative/runtime/modules/NotificationsModule.kt +100 -0
- pythonnative/native/android/src/main/java/com/pythonnative/runtime/modules/PermissionsModule.kt +99 -0
- pythonnative/native/android/src/main/java/com/pythonnative/runtime/modules/StorageModules.kt +89 -0
- pythonnative/native/android/src/main/java/com/pythonnative/runtime/modules/SystemModules.kt +178 -0
- pythonnative/native/android/src/main/java/com/pythonnative/runtime/plugins/GeneratedPlugins.kt +16 -0
- pythonnative/native/android/src/main/java/com/pythonnative/runtime/screens/PNScreenFragment.kt +188 -0
- pythonnative/native/android/src/main/java/com/pythonnative/runtime/screens/ScreenRegistry.kt +80 -0
- pythonnative/native/android/src/main/java/com/pythonnative/runtime/views/PNAccessibilityDelegate.kt +48 -0
- pythonnative/native/android/src/main/java/com/pythonnative/runtime/views/PNBorderDrawable.kt +76 -0
- pythonnative/native/android/src/main/java/com/pythonnative/runtime/views/PNEditText.kt +18 -0
- pythonnative/native/android/src/main/java/com/pythonnative/runtime/views/PNFrameLayout.kt +49 -0
- pythonnative/native/android/src/test/java/com/pythonnative/runtime/AnimationSpecsTest.kt +75 -0
- pythonnative/native/android/src/test/java/com/pythonnative/runtime/GestureArbiterTest.kt +197 -0
- pythonnative/native/android/src/test/java/com/pythonnative/runtime/PNColorTest.kt +47 -0
- pythonnative/native/android/src/test/java/com/pythonnative/runtime/PNTransactionTest.kt +78 -0
- pythonnative/native/android/src/test/java/com/pythonnative/runtime/PromiseTest.kt +89 -0
- pythonnative/native/ios/Package.swift +23 -0
- pythonnative/native/ios/Sources/PythonNativeKit/Animation/PNAnimationGraph.swift +199 -0
- pythonnative/native/ios/Sources/PythonNativeKit/Animation/PNAnimator.swift +312 -0
- pythonnative/native/ios/Sources/PythonNativeKit/Bridge/PNBridge.swift +255 -0
- pythonnative/native/ios/Sources/PythonNativeKit/Bridge/PNCommit.swift +104 -0
- pythonnative/native/ios/Sources/PythonNativeKit/Bridge/PNRegistry.swift +142 -0
- pythonnative/native/ios/Sources/PythonNativeKit/Bridge/PNTransaction.swift +166 -0
- pythonnative/native/ios/Sources/PythonNativeKit/Bridge/PNViewRegistry.swift +53 -0
- pythonnative/native/ios/Sources/PythonNativeKit/Components/PNButtonManagers.swift +207 -0
- pythonnative/native/ios/Sources/PythonNativeKit/Components/PNColor.swift +173 -0
- pythonnative/native/ios/Sources/PythonNativeKit/Components/PNComponentManager.swift +183 -0
- pythonnative/native/ios/Sources/PythonNativeKit/Components/PNContainerView.swift +56 -0
- pythonnative/native/ios/Sources/PythonNativeKit/Components/PNControlManagers.swift +281 -0
- pythonnative/native/ios/Sources/PythonNativeKit/Components/PNImageManager.swift +222 -0
- pythonnative/native/ios/Sources/PythonNativeKit/Components/PNOverlayManagers.swift +243 -0
- pythonnative/native/ios/Sources/PythonNativeKit/Components/PNPressableManager.swift +114 -0
- pythonnative/native/ios/Sources/PythonNativeKit/Components/PNScreenStackManager.swift +107 -0
- pythonnative/native/ios/Sources/PythonNativeKit/Components/PNScrollViewManager.swift +197 -0
- pythonnative/native/ios/Sources/PythonNativeKit/Components/PNTabBarManager.swift +106 -0
- pythonnative/native/ios/Sources/PythonNativeKit/Components/PNTextInputManager.swift +297 -0
- pythonnative/native/ios/Sources/PythonNativeKit/Components/PNTextManager.swift +230 -0
- pythonnative/native/ios/Sources/PythonNativeKit/Components/PNTransform.swift +82 -0
- pythonnative/native/ios/Sources/PythonNativeKit/Components/PNViewManager.swift +86 -0
- pythonnative/native/ios/Sources/PythonNativeKit/Components/PNViewState.swift +87 -0
- pythonnative/native/ios/Sources/PythonNativeKit/Components/PNViewStyler.swift +340 -0
- pythonnative/native/ios/Sources/PythonNativeKit/Components/PNVirtualListManager.swift +155 -0
- pythonnative/native/ios/Sources/PythonNativeKit/Components/PNWebViewManager.swift +124 -0
- pythonnative/native/ios/Sources/PythonNativeKit/Generated/NativeModules.swift +1 -0
- pythonnative/native/ios/Sources/PythonNativeKit/Generated/NativeProps.swift +3775 -0
- pythonnative/native/ios/Sources/PythonNativeKit/Generated/PNContracts.swift +46 -0
- pythonnative/native/ios/Sources/PythonNativeKit/Gestures/PNGestureCoordinator.swift +260 -0
- pythonnative/native/ios/Sources/PythonNativeKit/Layout/PNLayout.swift +173 -0
- pythonnative/native/ios/Sources/PythonNativeKit/Modules/AlertModule.swift +92 -0
- pythonnative/native/ios/Sources/PythonNativeKit/Modules/HostModule.swift +80 -0
- pythonnative/native/ios/Sources/PythonNativeKit/Modules/LifecycleModules.swift +174 -0
- pythonnative/native/ios/Sources/PythonNativeKit/Modules/MediaModules.swift +177 -0
- pythonnative/native/ios/Sources/PythonNativeKit/Modules/PNNativeModule.swift +168 -0
- pythonnative/native/ios/Sources/PythonNativeKit/Modules/PermissionModules.swift +266 -0
- pythonnative/native/ios/Sources/PythonNativeKit/Modules/SystemModules.swift +251 -0
- pythonnative/native/ios/Sources/PythonNativeKit/Plugins/PNPluginRegistration.swift +8 -0
- pythonnative/native/ios/Sources/PythonNativeKit/Screens/PNScreenRegistry.swift +46 -0
- pythonnative/native/ios/Sources/PythonNativeKit/Screens/PNViewController.swift +237 -0
- pythonnative/native/ios/Sources/PythonNativeKit/Support/PNCompat.swift +46 -0
- pythonnative/native/ios/Sources/PythonNativeKit/Support/PNJSON.swift +173 -0
- pythonnative/native/ios/Sources/PythonNativeKit/Support/PNLog.swift +47 -0
- pythonnative/native/ios/Sources/PythonNativeKit/Support/PNWindow.swift +51 -0
- pythonnative/native/ios/Tests/PythonNativeKitTests/PNAnimationGraphTests.swift +26 -0
- pythonnative/native/ios/Tests/PythonNativeKitTests/PNColorTests.swift +50 -0
- pythonnative/native/ios/Tests/PythonNativeKitTests/PNCommitTests.swift +86 -0
- pythonnative/native/ios/Tests/PythonNativeKitTests/PNGestureTests.swift +101 -0
- pythonnative/native/ios/Tests/PythonNativeKitTests/PNManagerTests.swift +126 -0
- pythonnative/native/ios/Tests/PythonNativeKitTests/PNModuleTests.swift +111 -0
- pythonnative/native/ios/Tests/PythonNativeKitTests/PNTransactionTests.swift +91 -0
- pythonnative/native/yoga/LICENSE +21 -0
- pythonnative/native/yoga/Package.swift +9 -0
- pythonnative/native/yoga/VENDORED.md +2 -0
- pythonnative/native/yoga/include/PNStyle.h +10 -0
- pythonnative/native/yoga/include/module.modulemap +5 -0
- pythonnative/native/yoga/include/yoga/YGConfig.h +158 -0
- pythonnative/native/yoga/include/yoga/YGEnums.h +142 -0
- pythonnative/native/yoga/include/yoga/YGMacros.h +99 -0
- pythonnative/native/yoga/include/yoga/YGNode.h +304 -0
- pythonnative/native/yoga/include/yoga/YGNodeLayout.h +35 -0
- pythonnative/native/yoga/include/yoga/YGNodeStyle.h +130 -0
- pythonnative/native/yoga/include/yoga/YGPixelGrid.h +29 -0
- pythonnative/native/yoga/include/yoga/YGValue.h +84 -0
- pythonnative/native/yoga/include/yoga/Yoga.h +21 -0
- pythonnative/native/yoga/python.cpp +16 -0
- pythonnative/native/yoga/style.cpp +272 -0
- pythonnative/native/yoga/yoga/CMakeLists.txt +41 -0
- pythonnative/native/yoga/yoga/YGConfig.cpp +92 -0
- pythonnative/native/yoga/yoga/YGConfig.h +158 -0
- pythonnative/native/yoga/yoga/YGEnums.cpp +262 -0
- pythonnative/native/yoga/yoga/YGEnums.h +142 -0
- pythonnative/native/yoga/yoga/YGMacros.h +99 -0
- pythonnative/native/yoga/yoga/YGNode.cpp +366 -0
- pythonnative/native/yoga/yoga/YGNode.h +304 -0
- pythonnative/native/yoga/yoga/YGNodeLayout.cpp +92 -0
- pythonnative/native/yoga/yoga/YGNodeLayout.h +35 -0
- pythonnative/native/yoga/yoga/YGNodeStyle.cpp +405 -0
- pythonnative/native/yoga/yoga/YGNodeStyle.h +130 -0
- pythonnative/native/yoga/yoga/YGPixelGrid.cpp +22 -0
- pythonnative/native/yoga/yoga/YGPixelGrid.h +29 -0
- pythonnative/native/yoga/yoga/YGValue.cpp +20 -0
- pythonnative/native/yoga/yoga/YGValue.h +84 -0
- pythonnative/native/yoga/yoga/Yoga.h +21 -0
- pythonnative/native/yoga/yoga/algorithm/AbsoluteLayout.cpp +563 -0
- pythonnative/native/yoga/yoga/algorithm/AbsoluteLayout.h +41 -0
- pythonnative/native/yoga/yoga/algorithm/Align.h +72 -0
- pythonnative/native/yoga/yoga/algorithm/Baseline.cpp +78 -0
- pythonnative/native/yoga/yoga/algorithm/Baseline.h +21 -0
- pythonnative/native/yoga/yoga/algorithm/BoundAxis.h +79 -0
- pythonnative/native/yoga/yoga/algorithm/Cache.cpp +121 -0
- pythonnative/native/yoga/yoga/algorithm/Cache.h +30 -0
- pythonnative/native/yoga/yoga/algorithm/CalculateLayout.cpp +2428 -0
- pythonnative/native/yoga/yoga/algorithm/CalculateLayout.h +38 -0
- pythonnative/native/yoga/yoga/algorithm/FlexDirection.h +120 -0
- pythonnative/native/yoga/yoga/algorithm/FlexLine.cpp +124 -0
- pythonnative/native/yoga/yoga/algorithm/FlexLine.h +75 -0
- pythonnative/native/yoga/yoga/algorithm/PixelGrid.cpp +132 -0
- pythonnative/native/yoga/yoga/algorithm/PixelGrid.h +29 -0
- pythonnative/native/yoga/yoga/algorithm/SizingMode.h +73 -0
- pythonnative/native/yoga/yoga/algorithm/TrailingPosition.h +44 -0
- pythonnative/native/yoga/yoga/config/Config.cpp +135 -0
- pythonnative/native/yoga/yoga/config/Config.h +92 -0
- pythonnative/native/yoga/yoga/debug/AssertFatal.cpp +53 -0
- pythonnative/native/yoga/yoga/debug/AssertFatal.h +29 -0
- pythonnative/native/yoga/yoga/debug/Log.cpp +107 -0
- pythonnative/native/yoga/yoga/debug/Log.h +34 -0
- pythonnative/native/yoga/yoga/enums/Align.h +47 -0
- pythonnative/native/yoga/yoga/enums/BoxSizing.h +40 -0
- pythonnative/native/yoga/yoga/enums/Dimension.h +40 -0
- pythonnative/native/yoga/yoga/enums/Direction.h +41 -0
- pythonnative/native/yoga/yoga/enums/Display.h +41 -0
- pythonnative/native/yoga/yoga/enums/Edge.h +47 -0
- pythonnative/native/yoga/yoga/enums/Errata.h +41 -0
- pythonnative/native/yoga/yoga/enums/ExperimentalFeature.h +39 -0
- pythonnative/native/yoga/yoga/enums/FlexDirection.h +42 -0
- pythonnative/native/yoga/yoga/enums/Gutter.h +41 -0
- pythonnative/native/yoga/yoga/enums/Justify.h +44 -0
- pythonnative/native/yoga/yoga/enums/LogLevel.h +44 -0
- pythonnative/native/yoga/yoga/enums/MeasureMode.h +41 -0
- pythonnative/native/yoga/yoga/enums/NodeType.h +40 -0
- pythonnative/native/yoga/yoga/enums/Overflow.h +41 -0
- pythonnative/native/yoga/yoga/enums/PhysicalEdge.h +21 -0
- pythonnative/native/yoga/yoga/enums/PositionType.h +41 -0
- pythonnative/native/yoga/yoga/enums/Unit.h +42 -0
- pythonnative/native/yoga/yoga/enums/Wrap.h +41 -0
- pythonnative/native/yoga/yoga/enums/YogaEnums.h +85 -0
- pythonnative/native/yoga/yoga/event/event.cpp +87 -0
- pythonnative/native/yoga/yoga/event/event.h +130 -0
- pythonnative/native/yoga/yoga/module.modulemap +21 -0
- pythonnative/native/yoga/yoga/node/CachedMeasurement.h +53 -0
- pythonnative/native/yoga/yoga/node/LayoutResults.cpp +48 -0
- pythonnative/native/yoga/yoga/node/LayoutResults.h +122 -0
- pythonnative/native/yoga/yoga/node/LayoutableChildren.h +148 -0
- pythonnative/native/yoga/yoga/node/Node.cpp +443 -0
- pythonnative/native/yoga/yoga/node/Node.h +337 -0
- pythonnative/native/yoga/yoga/numeric/Comparison.h +81 -0
- pythonnative/native/yoga/yoga/numeric/FloatOptional.h +93 -0
- pythonnative/native/yoga/yoga/style/SmallValueBuffer.h +133 -0
- pythonnative/native/yoga/yoga/style/Style.h +757 -0
- pythonnative/native/yoga/yoga/style/StyleLength.h +107 -0
- pythonnative/native/yoga/yoga/style/StyleValueHandle.h +98 -0
- pythonnative/native/yoga/yoga/style/StyleValuePool.h +127 -0
- pythonnative/native_modules/__init__.py +124 -0
- pythonnative/native_modules/app_state.py +99 -0
- pythonnative/native_modules/battery.py +75 -0
- pythonnative/native_modules/biometrics.py +42 -0
- pythonnative/native_modules/camera.py +65 -0
- pythonnative/native_modules/clipboard.py +46 -0
- pythonnative/native_modules/fallback.py +324 -0
- pythonnative/native_modules/file_system.py +145 -0
- pythonnative/native_modules/haptics.py +75 -0
- pythonnative/native_modules/linking.py +123 -0
- pythonnative/native_modules/location.py +75 -0
- pythonnative/native_modules/net_info.py +112 -0
- pythonnative/native_modules/notifications.py +129 -0
- pythonnative/native_modules/permissions.py +108 -0
- pythonnative/native_modules/registry.py +526 -0
- pythonnative/native_modules/secure_store.py +52 -0
- pythonnative/native_modules/share.py +51 -0
- pythonnative/native_views/__init__.py +375 -0
- pythonnative/native_views/base.py +316 -0
- pythonnative/native_views/bridge_backend.py +277 -0
- pythonnative/navigation/__init__.py +103 -0
- pythonnative/navigation/container.py +154 -0
- pythonnative/navigation/handle.py +540 -0
- pythonnative/navigation/hooks.py +133 -0
- pythonnative/navigation/host.py +58 -0
- pythonnative/navigation/linking.py +200 -0
- pythonnative/navigation/navigators.py +659 -0
- pythonnative/navigation/screen.py +149 -0
- pythonnative/navigation/state.py +269 -0
- pythonnative/net.py +248 -0
- pythonnative/platform.py +166 -0
- pythonnative/platform_metrics.py +252 -0
- pythonnative/preview.py +287 -0
- pythonnative/profiling.py +101 -0
- pythonnative/project/__init__.py +68 -0
- pythonnative/project/android.py +557 -0
- pythonnative/project/builder.py +817 -0
- pythonnative/project/config.py +773 -0
- pythonnative/project/deps.py +719 -0
- pythonnative/project/devices.py +304 -0
- pythonnative/project/doctor.py +283 -0
- pythonnative/project/fingerprint.py +171 -0
- pythonnative/project/icons.py +247 -0
- pythonnative/project/ios.py +325 -0
- pythonnative/project/lockfile.py +100 -0
- pythonnative/project/permissions.py +361 -0
- pythonnative/project/plugins.py +519 -0
- pythonnative/project/runtime_assets.py +183 -0
- pythonnative/py.typed +0 -0
- pythonnative/query.py +153 -0
- pythonnative/reconciler/__init__.py +29 -0
- pythonnative/reconciler/boundaries.py +375 -0
- pythonnative/reconciler/children.py +88 -0
- pythonnative/reconciler/core.py +1165 -0
- pythonnative/reconciler/layout_pass.py +393 -0
- pythonnative/reconciler/vnode.py +266 -0
- pythonnative/refresh.py +55 -0
- pythonnative/runtime.py +306 -0
- pythonnative/scheduler.py +159 -0
- pythonnative/sdk/__init__.py +153 -0
- pythonnative/sdk/_components.py +427 -0
- pythonnative/sdk/builtins.py +91 -0
- pythonnative/sdk/codegen.py +149 -0
- pythonnative/sdk/module_codegen.py +184 -0
- pythonnative/sdk/schema.py +224 -0
- pythonnative/sdk/templates/contracts.kt +46 -0
- pythonnative/sdk/templates/contracts.swift +46 -0
- pythonnative/storage.py +184 -0
- pythonnative/style.py +841 -0
- pythonnative/suspense.py +183 -0
- pythonnative/templates/android_template/app/build.gradle +75 -0
- pythonnative/templates/android_template/app/proguard-rules.pro +21 -0
- pythonnative/templates/android_template/app/src/androidTest/java/com/pythonnative/android_template/ExampleInstrumentedTest.kt +24 -0
- pythonnative/templates/android_template/app/src/debug/AndroidManifest.xml +7 -0
- pythonnative/templates/android_template/app/src/main/AndroidManifest.xml +32 -0
- pythonnative/templates/android_template/app/src/main/java/com/pythonnative/android_template/MainActivity.kt +130 -0
- pythonnative/templates/android_template/app/src/main/java/com/pythonnative/android_template/ScreenFragment.kt +13 -0
- pythonnative/templates/android_template/app/src/main/res/drawable/ic_launcher_background.xml +170 -0
- pythonnative/templates/android_template/app/src/main/res/drawable-v24/ic_launcher_foreground.xml +30 -0
- pythonnative/templates/android_template/app/src/main/res/layout/activity_main.xml +10 -0
- pythonnative/templates/android_template/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml +6 -0
- pythonnative/templates/android_template/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml +6 -0
- pythonnative/templates/android_template/app/src/main/res/mipmap-hdpi/ic_launcher.webp +0 -0
- pythonnative/templates/android_template/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp +0 -0
- pythonnative/templates/android_template/app/src/main/res/mipmap-mdpi/ic_launcher.webp +0 -0
- pythonnative/templates/android_template/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp +0 -0
- pythonnative/templates/android_template/app/src/main/res/mipmap-xhdpi/ic_launcher.webp +0 -0
- pythonnative/templates/android_template/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp +0 -0
- pythonnative/templates/android_template/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp +0 -0
- pythonnative/templates/android_template/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp +0 -0
- pythonnative/templates/android_template/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp +0 -0
- pythonnative/templates/android_template/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp +0 -0
- pythonnative/templates/android_template/app/src/main/res/navigation/nav_graph.xml +22 -0
- pythonnative/templates/android_template/app/src/main/res/values/colors.xml +5 -0
- pythonnative/templates/android_template/app/src/main/res/values/strings.xml +5 -0
- pythonnative/templates/android_template/app/src/main/res/values/themes.xml +9 -0
- pythonnative/templates/android_template/app/src/main/res/values-night/themes.xml +7 -0
- pythonnative/templates/android_template/app/src/main/res/xml/backup_rules.xml +13 -0
- pythonnative/templates/android_template/app/src/main/res/xml/data_extraction_rules.xml +19 -0
- pythonnative/templates/android_template/app/src/test/java/com/pythonnative/android_template/ExampleUnitTest.kt +17 -0
- pythonnative/templates/android_template/build.gradle +10 -0
- pythonnative/templates/android_template/gradle/wrapper/gradle-wrapper.jar +0 -0
- pythonnative/templates/android_template/gradle/wrapper/gradle-wrapper.properties +6 -0
- pythonnative/templates/android_template/gradle.properties +23 -0
- pythonnative/templates/android_template/gradlew +185 -0
- pythonnative/templates/android_template/gradlew.bat +89 -0
- pythonnative/templates/android_template/settings.gradle +17 -0
- pythonnative/templates/ios_template/ios_template/AppDelegate.swift +51 -0
- pythonnative/templates/ios_template/ios_template/Assets.xcassets/AccentColor.colorset/Contents.json +11 -0
- pythonnative/templates/ios_template/ios_template/Assets.xcassets/AppIcon.appiconset/Contents.json +13 -0
- pythonnative/templates/ios_template/ios_template/Assets.xcassets/Contents.json +6 -0
- pythonnative/templates/ios_template/ios_template/Base.lproj/LaunchScreen.storyboard +25 -0
- pythonnative/templates/ios_template/ios_template/BridgingHeader.h +14 -0
- pythonnative/templates/ios_template/ios_template/Info.plist +28 -0
- pythonnative/templates/ios_template/ios_template/PythonRuntime.swift +390 -0
- pythonnative/templates/ios_template/ios_template/SceneDelegate.swift +61 -0
- pythonnative/templates/ios_template/ios_template/ViewController.swift +23 -0
- pythonnative/templates/ios_template/ios_template.xcodeproj/project.pbxproj +745 -0
- pythonnative/templates/ios_template/ios_template.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +8 -0
- pythonnative/templates/ios_template/ios_templateTests/ios_templateTests.swift +36 -0
- pythonnative/templates/ios_template/ios_templateUITests/ios_templateUITests.swift +41 -0
- pythonnative/templates/ios_template/ios_templateUITests/ios_templateUITestsLaunchTests.swift +32 -0
- pythonnative/testing/__init__.py +53 -0
- pythonnative/testing/backend.py +276 -0
- pythonnative/testing/harness.py +369 -0
- pythonnative/utils.py +145 -0
- pythonnative-0.40.0.dist-info/DELVEWHEEL +2 -0
- pythonnative-0.40.0.dist-info/METADATA +150 -0
- pythonnative-0.40.0.dist-info/RECORD +418 -0
- pythonnative-0.40.0.dist-info/WHEEL +5 -0
- pythonnative-0.40.0.dist-info/entry_points.txt +2 -0
- pythonnative-0.40.0.dist-info/licenses/LICENSE +21 -0
- pythonnative-0.40.0.dist-info/top_level.txt +1 -0
- pythonnative.libs/msvcp140-a4c2229bdc2a2a630acdc095b4d86008.dll +0 -0
pythonnative/cli/pn.py
ADDED
|
@@ -0,0 +1,1214 @@
|
|
|
1
|
+
"""`pn` CLI: scaffold, diagnose, start, run, and build PythonNative apps.
|
|
2
|
+
|
|
3
|
+
The console script `pn` (declared in `pyproject.toml`) dispatches to:
|
|
4
|
+
|
|
5
|
+
- `pn init [name]`: scaffold a new project (``pythonnative.toml`` +
|
|
6
|
+
``app/``) into ``./name/``, or into the current directory when no name
|
|
7
|
+
is given.
|
|
8
|
+
- `pn doctor [platform]`: diagnose the local toolchain and config.
|
|
9
|
+
- `pn deps [platform]`: resolve ``[requirements].packages`` for every
|
|
10
|
+
device target and report which wheels would be used (or why a package
|
|
11
|
+
can't be installed), without building anything.
|
|
12
|
+
- `pn start`: run the dev server. It renders the app in a browser tab
|
|
13
|
+
and syncs every save to every connected debug build (simulators,
|
|
14
|
+
emulators, physical devices) with Fast Refresh; device logs stream
|
|
15
|
+
back into the same terminal.
|
|
16
|
+
- `pn preview`: `pn start` plus opening the browser preview.
|
|
17
|
+
- `pn devices [platform]`: list connected devices, emulators, and
|
|
18
|
+
simulators, as a table or as JSON with `--json`.
|
|
19
|
+
- `pn run android|ios [--device D]`: stage + build + install + launch a
|
|
20
|
+
debug build that connects to the dev server. The native project is
|
|
21
|
+
only rebuilt when something outside ``app/`` changed.
|
|
22
|
+
- `pn logs android|ios [--device D]`: stream logs from the running app
|
|
23
|
+
without rebuilding.
|
|
24
|
+
- `pn build android|ios`: produce standalone artifacts (signed APK/AAB,
|
|
25
|
+
or an iOS archive/IPA, optionally uploaded to App Store Connect).
|
|
26
|
+
- `pn app-id android|ios`: print the resolved application/bundle id
|
|
27
|
+
(handy for scripts and CI).
|
|
28
|
+
- `pn clean`: remove the local `build/` directory.
|
|
29
|
+
|
|
30
|
+
The heavy lifting lives in the ``pythonnative.project`` and
|
|
31
|
+
``pythonnative.devserver`` packages; this module is a thin,
|
|
32
|
+
side-effect-y shell that wires arguments to them and handles the
|
|
33
|
+
device-facing steps (simulator boot, launch, log streaming) that can't
|
|
34
|
+
be unit tested.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
from __future__ import annotations
|
|
38
|
+
|
|
39
|
+
import argparse
|
|
40
|
+
import dataclasses
|
|
41
|
+
import json
|
|
42
|
+
import os
|
|
43
|
+
import re
|
|
44
|
+
import shutil
|
|
45
|
+
import subprocess
|
|
46
|
+
import sys
|
|
47
|
+
from importlib.metadata import version as pkg_version
|
|
48
|
+
from pathlib import Path
|
|
49
|
+
from typing import Any, Dict, List, Optional, Sequence, TextIO
|
|
50
|
+
from urllib.request import urlopen
|
|
51
|
+
|
|
52
|
+
from ..project import builder as builder_mod
|
|
53
|
+
from ..project import deps as deps_mod
|
|
54
|
+
from ..project import devices as devices_mod
|
|
55
|
+
from ..project import doctor as doctor_mod
|
|
56
|
+
from ..project import fingerprint as fingerprint_mod
|
|
57
|
+
from ..project.android import collect_logcat_filters
|
|
58
|
+
from ..project.config import CONFIG_FILENAME, AppConfig, ConfigError, render_default_toml
|
|
59
|
+
|
|
60
|
+
DEFAULT_DEV_PORT = 8765
|
|
61
|
+
"""Port `pn start` listens on unless `--port` says otherwise."""
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
# ======================================================================
|
|
65
|
+
# init
|
|
66
|
+
# ======================================================================
|
|
67
|
+
|
|
68
|
+
_MAIN_TEMPLATE = """from typing import TypedDict
|
|
69
|
+
|
|
70
|
+
import pythonnative as pn
|
|
71
|
+
|
|
72
|
+
Stack = pn.create_stack_navigator()
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class DetailParams(TypedDict):
|
|
76
|
+
count: int
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
@pn.component
|
|
80
|
+
def HomeScreen():
|
|
81
|
+
count, set_count = pn.use_state(0)
|
|
82
|
+
nav = pn.use_navigation()
|
|
83
|
+
theme = pn.use_theme()
|
|
84
|
+
return pn.ScrollView(
|
|
85
|
+
pn.Column(
|
|
86
|
+
pn.Text("Hello from PythonNative!", style={"font_size": theme.font_size_title, "bold": True}),
|
|
87
|
+
pn.Text(f"Tapped {count} times"),
|
|
88
|
+
pn.Button("Tap me", on_press=lambda: set_count(count + 1)),
|
|
89
|
+
pn.Button("Open detail", on_press=lambda: nav.navigate("Detail", count=count)),
|
|
90
|
+
style={"spacing": theme.spacing_large, "padding": 16, "align_items": "stretch"},
|
|
91
|
+
)
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
@pn.component
|
|
96
|
+
def DetailScreen():
|
|
97
|
+
nav = pn.use_navigation()
|
|
98
|
+
route = pn.use_route(DetailParams)
|
|
99
|
+
return pn.Column(
|
|
100
|
+
pn.Text(f"Detail: count was {route.params['count']}", style={"font_size": 20}),
|
|
101
|
+
pn.Button("Back", on_press=nav.go_back),
|
|
102
|
+
style={"spacing": 12, "padding": 16},
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
@pn.component
|
|
107
|
+
def App():
|
|
108
|
+
return pn.NavigationContainer(
|
|
109
|
+
Stack.Navigator(
|
|
110
|
+
Stack.Screen("Home", HomeScreen, title="Home"),
|
|
111
|
+
Stack.Screen("Detail", DetailScreen, title="Detail"),
|
|
112
|
+
)
|
|
113
|
+
)
|
|
114
|
+
"""
|
|
115
|
+
|
|
116
|
+
_GITIGNORE = "# PythonNative\n__pycache__/\n*.pyc\n.venv/\nbuild/\n.DS_Store\n"
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
_NAME_RE = re.compile(r"^[a-z][a-z0-9_-]*$")
|
|
120
|
+
"""Legal ``pn init`` project names, in the spirit of ``flutter create`` / ``cargo new``."""
|
|
121
|
+
|
|
122
|
+
_FALLBACK_NAME = "my_app"
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _sanitize_name(name: str) -> str:
|
|
126
|
+
"""Return a legal project name derived from ``name``.
|
|
127
|
+
|
|
128
|
+
Lowercases, collapses each run of illegal characters to one
|
|
129
|
+
underscore, trims leading and trailing ``_`` and ``-``, and prefixes
|
|
130
|
+
a name that doesn't start with a letter. The result always matches
|
|
131
|
+
``_NAME_RE``, falling back to ``_FALLBACK_NAME`` when nothing usable
|
|
132
|
+
survives.
|
|
133
|
+
|
|
134
|
+
Args:
|
|
135
|
+
name: The rejected name, which may be empty.
|
|
136
|
+
|
|
137
|
+
Returns:
|
|
138
|
+
A name suitable for suggesting back to the user.
|
|
139
|
+
"""
|
|
140
|
+
slug = re.sub(r"[^a-z0-9_-]+", "_", name.lower()).strip("_-")
|
|
141
|
+
if not slug:
|
|
142
|
+
return _FALLBACK_NAME
|
|
143
|
+
if not slug[0].isascii() or not slug[0].isalpha():
|
|
144
|
+
slug = f"app_{slug}"
|
|
145
|
+
return slug
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def _app_id_from_name(name: str) -> str:
|
|
149
|
+
slug = re.sub(r"[^a-z0-9_]", "", name.lower())
|
|
150
|
+
if not slug or not slug[0].isalpha():
|
|
151
|
+
slug = "app" + slug
|
|
152
|
+
return f"com.example.{slug}"
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def init_project(args: argparse.Namespace) -> None:
|
|
156
|
+
"""Scaffold a new PythonNative project.
|
|
157
|
+
|
|
158
|
+
Given a name, this creates ``./<name>/`` and scaffolds into it. Without
|
|
159
|
+
one, it scaffolds into the current directory and names the project after
|
|
160
|
+
it. Either way it writes ``app/main.py``, ``pythonnative.toml``, and
|
|
161
|
+
``.gitignore``.
|
|
162
|
+
|
|
163
|
+
A name you pass has to match ``^[a-z][a-z0-9_-]*$``: lowercase letters,
|
|
164
|
+
digits, ``-``, and ``_``, starting with a letter. Anything else is
|
|
165
|
+
refused with a legal suggestion. That keeps the directory name and the
|
|
166
|
+
``name`` field in the generated config identical, in the same spirit as
|
|
167
|
+
``flutter create`` and ``cargo new``. The name taken from the current
|
|
168
|
+
directory when you pass none is used as-is, so an existing directory
|
|
169
|
+
with any name still works.
|
|
170
|
+
|
|
171
|
+
The name also has to be a single directory name, so the project always
|
|
172
|
+
lands inside the current directory. Anything that reads as a path, such
|
|
173
|
+
as ``a/b``, ``..``, or ``/tmp/app``, is refused, and so is a name that
|
|
174
|
+
resolves somewhere else, such as a symlink to another directory.
|
|
175
|
+
|
|
176
|
+
It won't scaffold into a target directory that already holds files, and it
|
|
177
|
+
won't overwrite any of the three paths above; pass ``--force`` to override
|
|
178
|
+
both. An existing but empty target directory is fine. A plain file at
|
|
179
|
+
``./<name>`` is always refused, since ``--force`` can't turn it into a
|
|
180
|
+
directory, and ``--force`` lifts neither of the rules above.
|
|
181
|
+
|
|
182
|
+
Args:
|
|
183
|
+
args: Parsed namespace with ``name`` (optional) and ``force``.
|
|
184
|
+
"""
|
|
185
|
+
name: Optional[str] = getattr(args, "name", None)
|
|
186
|
+
force: bool = getattr(args, "force", False)
|
|
187
|
+
|
|
188
|
+
# Lexical check, before anything reads the filesystem. ``Path("..").name``
|
|
189
|
+
# is "..", so ".." needs naming explicitly; the rest (absolute, nested,
|
|
190
|
+
# trailing separator, ".") fall out of the name check.
|
|
191
|
+
if name and (name in (os.curdir, os.pardir) or Path(name).name != name):
|
|
192
|
+
print(f"Refusing to treat a path as a project name: {name!r}. Use a single directory name like my_app.")
|
|
193
|
+
sys.exit(1)
|
|
194
|
+
|
|
195
|
+
# Charset check, still lexical, so it stays ahead of ``Path.cwd()`` below.
|
|
196
|
+
# ``is not None`` rather than truthiness: "" is invalid under the pattern,
|
|
197
|
+
# and falling through to the no-name path would silently scaffold here.
|
|
198
|
+
# ``fullmatch``, not ``match``: ``$`` also matches before a trailing
|
|
199
|
+
# newline, so ``match`` would accept "app\n" and create a directory
|
|
200
|
+
# whose name contains one.
|
|
201
|
+
if name is not None and not _NAME_RE.fullmatch(name):
|
|
202
|
+
print(
|
|
203
|
+
f"Invalid project name: {name!r}. Use lowercase letters, digits, '-', and '_', "
|
|
204
|
+
f"starting with a letter. Try: {_sanitize_name(name)}"
|
|
205
|
+
)
|
|
206
|
+
sys.exit(1)
|
|
207
|
+
|
|
208
|
+
cwd = Path.cwd()
|
|
209
|
+
target = cwd / name if name else cwd
|
|
210
|
+
project_name: str = name or cwd.name
|
|
211
|
+
|
|
212
|
+
# A lexically clean name can still resolve elsewhere, and ``exists()`` and
|
|
213
|
+
# ``is_dir()`` below follow symlinks. Check containment rather than just
|
|
214
|
+
# ``is_symlink()`` so the whole class is closed, not one spelling of it.
|
|
215
|
+
if name and (target.is_symlink() or target.resolve().parent != cwd.resolve()):
|
|
216
|
+
print(
|
|
217
|
+
f"Refusing to scaffold through a link or outside the current directory: {name}. Use a plain directory name."
|
|
218
|
+
)
|
|
219
|
+
sys.exit(1)
|
|
220
|
+
|
|
221
|
+
app_dir = target / "app"
|
|
222
|
+
config_path = target / CONFIG_FILENAME
|
|
223
|
+
gitignore_path = target / ".gitignore"
|
|
224
|
+
|
|
225
|
+
if name and target.exists():
|
|
226
|
+
if not target.is_dir():
|
|
227
|
+
print(f"Refusing to overwrite existing file: {name}. Remove it or choose a different name.")
|
|
228
|
+
sys.exit(1)
|
|
229
|
+
if any(target.iterdir()) and not force:
|
|
230
|
+
print(f"Refusing to overwrite existing non-empty directory: {name}/. Use --force to overwrite.")
|
|
231
|
+
sys.exit(1)
|
|
232
|
+
|
|
233
|
+
if not force:
|
|
234
|
+
existing = [
|
|
235
|
+
label
|
|
236
|
+
for label, path in (("app/", app_dir), (CONFIG_FILENAME, config_path), (".gitignore", gitignore_path))
|
|
237
|
+
if path.exists()
|
|
238
|
+
]
|
|
239
|
+
if existing:
|
|
240
|
+
print(f"Refusing to overwrite existing: {', '.join(existing)}. Use --force to overwrite.")
|
|
241
|
+
sys.exit(1)
|
|
242
|
+
|
|
243
|
+
app_dir.mkdir(parents=True, exist_ok=True)
|
|
244
|
+
main_py = app_dir / "main.py"
|
|
245
|
+
if force or not main_py.exists():
|
|
246
|
+
main_py.write_text(_MAIN_TEMPLATE, encoding="utf-8")
|
|
247
|
+
|
|
248
|
+
config_path.write_text(
|
|
249
|
+
render_default_toml(name=project_name, app_id=_app_id_from_name(project_name)),
|
|
250
|
+
encoding="utf-8",
|
|
251
|
+
)
|
|
252
|
+
if force or not gitignore_path.exists():
|
|
253
|
+
gitignore_path.write_text(_GITIGNORE, encoding="utf-8")
|
|
254
|
+
|
|
255
|
+
print(f"Initialized PythonNative project in {target}.")
|
|
256
|
+
next_steps = "pn start (browser preview + dev server) | pn run android | pn run ios"
|
|
257
|
+
if name:
|
|
258
|
+
next_steps = f"cd {name} | {next_steps}"
|
|
259
|
+
print(f"Next: {next_steps}")
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
# ======================================================================
|
|
263
|
+
# doctor / app-id
|
|
264
|
+
# ======================================================================
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def doctor_command(args: argparse.Namespace) -> None:
|
|
268
|
+
"""Run toolchain/config diagnostics and exit non-zero on errors.
|
|
269
|
+
|
|
270
|
+
Args:
|
|
271
|
+
args: Parsed namespace with optional ``platform``.
|
|
272
|
+
"""
|
|
273
|
+
platform: Optional[str] = getattr(args, "platform", None)
|
|
274
|
+
results = doctor_mod.run_doctor(Path.cwd(), platform=platform)
|
|
275
|
+
print("PythonNative doctor\n")
|
|
276
|
+
for result in results:
|
|
277
|
+
print(result.format())
|
|
278
|
+
level = doctor_mod.worst_level(results)
|
|
279
|
+
print()
|
|
280
|
+
if level == doctor_mod.ERROR:
|
|
281
|
+
print("Found problems that will block builds. Address the [x] items above.")
|
|
282
|
+
sys.exit(1)
|
|
283
|
+
if level == doctor_mod.WARN:
|
|
284
|
+
print("Ready, with warnings. Review the [!] items above.")
|
|
285
|
+
else:
|
|
286
|
+
print("Everything looks good.")
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
def app_id_command(args: argparse.Namespace) -> None:
|
|
290
|
+
"""Print the resolved application id (Android) or bundle id (iOS).
|
|
291
|
+
|
|
292
|
+
Args:
|
|
293
|
+
args: Parsed namespace with ``platform``.
|
|
294
|
+
"""
|
|
295
|
+
config = _load_config_or_exit()
|
|
296
|
+
print(config.application_id if args.platform == "android" else config.bundle_id)
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
# ======================================================================
|
|
300
|
+
# deps
|
|
301
|
+
# ======================================================================
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
def deps_command(args: argparse.Namespace) -> None:
|
|
305
|
+
"""Report how ``[requirements].packages`` resolve for each device target.
|
|
306
|
+
|
|
307
|
+
Runs pip in its cross-platform dry-run mode once per target (iOS
|
|
308
|
+
device, iOS Simulator, and one per Android ABI) and prints the
|
|
309
|
+
wheel each package would use, flagging binary wheels and their
|
|
310
|
+
source index. Exits non-zero when any target can't be satisfied,
|
|
311
|
+
so it doubles as a CI gate. ``--json`` emits the same data as a
|
|
312
|
+
machine-readable document.
|
|
313
|
+
|
|
314
|
+
Args:
|
|
315
|
+
args: Parsed namespace with optional ``platform``, ``json``, and
|
|
316
|
+
``python`` (the interpreter to run pip with).
|
|
317
|
+
"""
|
|
318
|
+
platform: Optional[str] = getattr(args, "platform", None)
|
|
319
|
+
as_json: bool = getattr(args, "json", False)
|
|
320
|
+
python: Optional[str] = getattr(args, "python", None)
|
|
321
|
+
|
|
322
|
+
config = _load_config_or_exit()
|
|
323
|
+
targets = deps_mod.targets_for(config, platform, all_simulator_archs=getattr(args, "lock", False))
|
|
324
|
+
runner = builder_mod.SubprocessRunner()
|
|
325
|
+
if not as_json and config.requirements:
|
|
326
|
+
print(
|
|
327
|
+
f"Resolving {len(config.requirements)} requirement(s) for Python {config.python_version} "
|
|
328
|
+
f"across {len(targets)} target(s)...\n"
|
|
329
|
+
)
|
|
330
|
+
resolutions = deps_mod.resolve_all(config, targets, runner=runner, python=python)
|
|
331
|
+
if getattr(args, "lock", False):
|
|
332
|
+
from ..project.lockfile import write
|
|
333
|
+
|
|
334
|
+
path = write(config, resolutions)
|
|
335
|
+
if not as_json:
|
|
336
|
+
print(f"Wrote {path}")
|
|
337
|
+
|
|
338
|
+
if as_json:
|
|
339
|
+
print(
|
|
340
|
+
json.dumps(
|
|
341
|
+
{
|
|
342
|
+
"python_version": config.python_version,
|
|
343
|
+
"requirements": list(config.requirements),
|
|
344
|
+
"targets": [res.to_dict() for res in resolutions],
|
|
345
|
+
},
|
|
346
|
+
indent=2,
|
|
347
|
+
)
|
|
348
|
+
)
|
|
349
|
+
else:
|
|
350
|
+
print(deps_mod.format_report(resolutions, requirements=config.requirements))
|
|
351
|
+
if any(not res.ok for res in resolutions):
|
|
352
|
+
sys.exit(1)
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
# ======================================================================
|
|
356
|
+
# start / preview
|
|
357
|
+
# ======================================================================
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
def start_command(args: argparse.Namespace, *, open_browser: bool = False) -> None:
|
|
361
|
+
"""Run the dev server (and the browser preview) for the current project.
|
|
362
|
+
|
|
363
|
+
Re-execs under ``PN_PLATFORM=web`` so every module binds to the
|
|
364
|
+
browser backend, then hands off to ``pythonnative.preview.serve``.
|
|
365
|
+
|
|
366
|
+
Args:
|
|
367
|
+
args: Parsed namespace (``entry``, ``host``, ``port``, ``open``,
|
|
368
|
+
``no_open``).
|
|
369
|
+
open_browser: Open the preview page once the server is up
|
|
370
|
+
(``pn preview`` sets this; ``--open`` does too).
|
|
371
|
+
"""
|
|
372
|
+
if os.environ.get("PN_PLATFORM") != "web":
|
|
373
|
+
try:
|
|
374
|
+
completed = subprocess.run(
|
|
375
|
+
[sys.executable, "-m", "pythonnative.cli.pn", *sys.argv[1:]],
|
|
376
|
+
env={**os.environ, "PN_PLATFORM": "web"},
|
|
377
|
+
)
|
|
378
|
+
except KeyboardInterrupt:
|
|
379
|
+
sys.exit(130)
|
|
380
|
+
sys.exit(completed.returncode)
|
|
381
|
+
|
|
382
|
+
project_dir = Path.cwd()
|
|
383
|
+
entry: Optional[str] = getattr(args, "entry", None)
|
|
384
|
+
project_name = ""
|
|
385
|
+
requirements: List[str] = []
|
|
386
|
+
try:
|
|
387
|
+
config = AppConfig.load(project_dir)
|
|
388
|
+
entry = entry or config.entry_module
|
|
389
|
+
project_name = config.name
|
|
390
|
+
requirements = list(config.requirements)
|
|
391
|
+
except ConfigError:
|
|
392
|
+
entry = entry or "app.main"
|
|
393
|
+
missing = _missing_requirements(requirements)
|
|
394
|
+
if missing:
|
|
395
|
+
print(f"Warning: {', '.join(missing)} from [requirements] is not installed in this Python environment.")
|
|
396
|
+
print(
|
|
397
|
+
" The browser preview imports your app here, so install it first (e.g. `pip install "
|
|
398
|
+
+ " ".join(missing)
|
|
399
|
+
+ "`)."
|
|
400
|
+
)
|
|
401
|
+
if not (project_dir / "app").is_dir():
|
|
402
|
+
print(f"Error: no app/ directory in {project_dir}. Run 'pn init' first or cd into a PythonNative project.")
|
|
403
|
+
sys.exit(1)
|
|
404
|
+
|
|
405
|
+
from pythonnative.preview import serve
|
|
406
|
+
|
|
407
|
+
open_browser = open_browser or bool(getattr(args, "open", False))
|
|
408
|
+
if getattr(args, "no_open", False):
|
|
409
|
+
open_browser = False
|
|
410
|
+
try:
|
|
411
|
+
serve(
|
|
412
|
+
entry,
|
|
413
|
+
project_root=str(project_dir),
|
|
414
|
+
host=getattr(args, "host", "0.0.0.0") or "0.0.0.0",
|
|
415
|
+
port=int(getattr(args, "port", DEFAULT_DEV_PORT) or 0),
|
|
416
|
+
project_name=project_name,
|
|
417
|
+
open_browser=open_browser,
|
|
418
|
+
)
|
|
419
|
+
except OSError as exc:
|
|
420
|
+
print(f"Error: could not start the dev server: {exc}")
|
|
421
|
+
print("Is another 'pn start' running? Pick a different port with --port.")
|
|
422
|
+
sys.exit(1)
|
|
423
|
+
except RuntimeError as exc:
|
|
424
|
+
print(f"Error: {exc}")
|
|
425
|
+
sys.exit(1)
|
|
426
|
+
|
|
427
|
+
|
|
428
|
+
def _missing_requirements(requirements: Sequence[str]) -> List[str]:
|
|
429
|
+
"""Names from ``[requirements]`` that aren't installed in this interpreter.
|
|
430
|
+
|
|
431
|
+
Requirement strings may carry version specifiers or extras
|
|
432
|
+
(``"httpx[http2]>=0.27"``); only the distribution name is checked.
|
|
433
|
+
"""
|
|
434
|
+
import importlib.metadata as metadata
|
|
435
|
+
|
|
436
|
+
missing: List[str] = []
|
|
437
|
+
for requirement in requirements:
|
|
438
|
+
name = re.split(r"[\s\[<>=!~;@]", requirement.strip(), maxsplit=1)[0]
|
|
439
|
+
if not name:
|
|
440
|
+
continue
|
|
441
|
+
try:
|
|
442
|
+
metadata.distribution(name)
|
|
443
|
+
except metadata.PackageNotFoundError:
|
|
444
|
+
missing.append(name)
|
|
445
|
+
return missing
|
|
446
|
+
|
|
447
|
+
|
|
448
|
+
def preview_command(args: argparse.Namespace) -> None:
|
|
449
|
+
"""``pn preview``: ``pn start`` that also opens the browser preview."""
|
|
450
|
+
start_command(args, open_browser=True)
|
|
451
|
+
|
|
452
|
+
|
|
453
|
+
def _running_dev_server(port: int) -> Optional[Dict[str, Any]]:
|
|
454
|
+
"""Return ``/status`` of a dev server on ``localhost:port``, or ``None``."""
|
|
455
|
+
try:
|
|
456
|
+
with urlopen(f"http://127.0.0.1:{port}/status", timeout=0.5) as response:
|
|
457
|
+
data = json.loads(response.read().decode("utf-8"))
|
|
458
|
+
except Exception:
|
|
459
|
+
return None
|
|
460
|
+
return data if isinstance(data, dict) else None
|
|
461
|
+
|
|
462
|
+
|
|
463
|
+
# ======================================================================
|
|
464
|
+
# devices
|
|
465
|
+
# ======================================================================
|
|
466
|
+
|
|
467
|
+
|
|
468
|
+
def _print_no_devices_hints(stream: TextIO) -> None:
|
|
469
|
+
"""Print the "no devices" guidance to ``stream``.
|
|
470
|
+
|
|
471
|
+
Shared by both output modes so the wording can't drift between the
|
|
472
|
+
table (which sends it to stdout) and ``--json`` (stderr).
|
|
473
|
+
|
|
474
|
+
Args:
|
|
475
|
+
stream: Where to write, ``sys.stdout`` or ``sys.stderr``.
|
|
476
|
+
"""
|
|
477
|
+
print("No devices found.", file=stream)
|
|
478
|
+
print("Android: start an emulator or connect a device with USB debugging enabled.", file=stream)
|
|
479
|
+
print("iOS: open Xcode once to install Simulators, or plug in a device.", file=stream)
|
|
480
|
+
|
|
481
|
+
|
|
482
|
+
def devices_command(args: argparse.Namespace) -> None:
|
|
483
|
+
"""List connected devices, emulators, and simulators.
|
|
484
|
+
|
|
485
|
+
Prints an aligned table and exits 1 when nothing is connected.
|
|
486
|
+
|
|
487
|
+
With ``--json``, stdout carries a JSON array and nothing else, one
|
|
488
|
+
object per device (see ``Device.to_dict``), so it stays parseable.
|
|
489
|
+
The "no devices" hints go to stderr instead, an empty result prints
|
|
490
|
+
``[]``, and the exit status is 0 either way, since "no devices" is a
|
|
491
|
+
valid answer for a script rather than a failure.
|
|
492
|
+
|
|
493
|
+
Args:
|
|
494
|
+
args: Parsed namespace with optional ``platform`` and ``json``.
|
|
495
|
+
"""
|
|
496
|
+
platform: Optional[str] = getattr(args, "platform", None)
|
|
497
|
+
as_json: bool = getattr(args, "json", False)
|
|
498
|
+
devices = devices_mod.list_devices(platform)
|
|
499
|
+
|
|
500
|
+
if as_json:
|
|
501
|
+
if not devices:
|
|
502
|
+
_print_no_devices_hints(sys.stderr)
|
|
503
|
+
print(json.dumps([device.to_dict() for device in devices], indent=2))
|
|
504
|
+
return
|
|
505
|
+
|
|
506
|
+
if not devices:
|
|
507
|
+
_print_no_devices_hints(sys.stdout)
|
|
508
|
+
sys.exit(1)
|
|
509
|
+
print(f" {'IDENTIFIER':<40} {'KIND':<10} {'STATE':<10} NAME")
|
|
510
|
+
for device in devices:
|
|
511
|
+
print(device.format())
|
|
512
|
+
print("\nTarget one with: pn run <platform> --device <identifier or name>")
|
|
513
|
+
|
|
514
|
+
|
|
515
|
+
def _resolve_device(platform: str, query: Optional[str]) -> Optional[devices_mod.Device]:
|
|
516
|
+
"""Resolve ``--device`` to a concrete target, exiting on a bad query."""
|
|
517
|
+
if not query:
|
|
518
|
+
return None
|
|
519
|
+
device = devices_mod.find_device(devices_mod.list_devices(platform), query)
|
|
520
|
+
if device is None:
|
|
521
|
+
print(f"Error: no {platform} device matches {query!r}. Run 'pn devices {platform}' to list targets.")
|
|
522
|
+
sys.exit(1)
|
|
523
|
+
return device
|
|
524
|
+
|
|
525
|
+
|
|
526
|
+
# ======================================================================
|
|
527
|
+
# run
|
|
528
|
+
# ======================================================================
|
|
529
|
+
|
|
530
|
+
|
|
531
|
+
def _dev_server_url_for(platform: str, device: Optional[devices_mod.Device], port: int) -> str:
|
|
532
|
+
"""The WebSocket URL a launched app should use to reach ``pn start``.
|
|
533
|
+
|
|
534
|
+
Simulators share the host's loopback. Android emulators and USB
|
|
535
|
+
devices reach it through ``adb reverse`` (set up by the caller), so
|
|
536
|
+
``localhost`` works for every Android target. A physical iOS device
|
|
537
|
+
is on the LAN, so it gets the first LAN address.
|
|
538
|
+
"""
|
|
539
|
+
if platform == "ios" and device is not None and device.kind == "device":
|
|
540
|
+
from ..devserver import lan_addresses
|
|
541
|
+
|
|
542
|
+
for address in lan_addresses():
|
|
543
|
+
return f"ws://{address}:{port}/ws?role=client"
|
|
544
|
+
return f"ws://localhost:{port}/ws?role=client"
|
|
545
|
+
|
|
546
|
+
|
|
547
|
+
def run_project(args: argparse.Namespace) -> None:
|
|
548
|
+
"""Stage, build, install, and launch a debug build that talks to ``pn start``.
|
|
549
|
+
|
|
550
|
+
The native toolchain only runs when a native input changed (see
|
|
551
|
+
``pythonnative.project.fingerprint``) or when no dev server is up
|
|
552
|
+
to deliver the latest sources; otherwise the previous artifact is
|
|
553
|
+
reinstalled, which turns the edit/relaunch loop from minutes into
|
|
554
|
+
seconds.
|
|
555
|
+
|
|
556
|
+
Args:
|
|
557
|
+
args: Parsed namespace (``platform``, ``device``,
|
|
558
|
+
``prepare_only``, ``no_logs``, ``rebuild``, ``dev_server``,
|
|
559
|
+
``port``, ``dev_client``).
|
|
560
|
+
"""
|
|
561
|
+
platform: str = args.platform
|
|
562
|
+
prepare_only: bool = getattr(args, "prepare_only", False)
|
|
563
|
+
show_logs: bool = not getattr(args, "no_logs", False)
|
|
564
|
+
force_rebuild: bool = getattr(args, "rebuild", False)
|
|
565
|
+
dev_client: bool = getattr(args, "dev_client", False)
|
|
566
|
+
port: int = int(getattr(args, "port", DEFAULT_DEV_PORT) or DEFAULT_DEV_PORT)
|
|
567
|
+
device = _resolve_device(platform, getattr(args, "device", None))
|
|
568
|
+
|
|
569
|
+
config = _load_config_or_exit()
|
|
570
|
+
if dev_client:
|
|
571
|
+
# A dev client is the same native app whose entry module is the
|
|
572
|
+
# connect screen; the real app arrives from the dev server.
|
|
573
|
+
config = dataclasses.replace(config, entry_point="pythonnative/devclient.py")
|
|
574
|
+
builder = builder_mod.Builder(config, log=print)
|
|
575
|
+
|
|
576
|
+
# Resolve third-party packages only for the destination being built
|
|
577
|
+
# (device wheels and Simulator wheels differ); prepare-only keeps both
|
|
578
|
+
# slices so the staged project builds for either in Xcode.
|
|
579
|
+
if prepare_only:
|
|
580
|
+
ios_sdks: tuple = deps_mod.IOS_SDKS
|
|
581
|
+
elif device is not None and device.kind == "device":
|
|
582
|
+
ios_sdks = ("iphoneos",)
|
|
583
|
+
else:
|
|
584
|
+
ios_sdks = ("iphonesimulator",)
|
|
585
|
+
|
|
586
|
+
explicit_server: Optional[str] = getattr(args, "dev_server", None)
|
|
587
|
+
status = _running_dev_server(port) if not explicit_server else None
|
|
588
|
+
if explicit_server:
|
|
589
|
+
server_url: Optional[str] = explicit_server
|
|
590
|
+
elif status is not None:
|
|
591
|
+
server_url = _dev_server_url_for(platform, device, int(status.get("port") or port))
|
|
592
|
+
else:
|
|
593
|
+
server_url = None
|
|
594
|
+
if not prepare_only:
|
|
595
|
+
print(
|
|
596
|
+
f"Note: no dev server on port {port}. Run 'pn start' in another terminal and relaunch, or the "
|
|
597
|
+
"app will run its bundled sources without Fast Refresh."
|
|
598
|
+
)
|
|
599
|
+
|
|
600
|
+
fingerprint = _native_fingerprint(config, platform, builder, ios_sdks=ios_sdks, dev_client=dev_client)
|
|
601
|
+
build_dir = builder.build_root / platform
|
|
602
|
+
stamp = fingerprint_mod.read_stamp(build_dir)
|
|
603
|
+
artifact = Path(stamp["artifact"]) if stamp and stamp.get("artifact") else None
|
|
604
|
+
reuse = (
|
|
605
|
+
not prepare_only
|
|
606
|
+
and not force_rebuild
|
|
607
|
+
and server_url is not None
|
|
608
|
+
and stamp is not None
|
|
609
|
+
and stamp.get("fingerprint") == fingerprint
|
|
610
|
+
and artifact is not None
|
|
611
|
+
and artifact.exists()
|
|
612
|
+
)
|
|
613
|
+
|
|
614
|
+
prepared: Optional[builder_mod.PreparedProject] = None
|
|
615
|
+
if reuse:
|
|
616
|
+
print(f"Native inputs unchanged; reinstalling {artifact} (use --rebuild to force a build).")
|
|
617
|
+
else:
|
|
618
|
+
try:
|
|
619
|
+
prepared = builder.prepare(platform, ios_sdks=ios_sdks)
|
|
620
|
+
except builder_mod.BuildError as exc:
|
|
621
|
+
print(f"Error: {exc}")
|
|
622
|
+
sys.exit(1)
|
|
623
|
+
if prepare_only:
|
|
624
|
+
print(f"Prepared {platform} project in {prepared.project_dir} (prepare-only).")
|
|
625
|
+
return
|
|
626
|
+
|
|
627
|
+
app_id = config.application_id if platform == "android" else config.bundle_id
|
|
628
|
+
if server_url:
|
|
629
|
+
print(f"Dev server: {server_url} (saves in app/ apply with Fast Refresh).")
|
|
630
|
+
try:
|
|
631
|
+
if platform == "android":
|
|
632
|
+
artifact = _run_android(
|
|
633
|
+
builder, prepared, artifact=artifact, app_id=app_id, device=device, server_url=server_url, port=port
|
|
634
|
+
)
|
|
635
|
+
elif device is not None and device.kind == "device":
|
|
636
|
+
artifact = _run_ios_device(
|
|
637
|
+
builder, prepared, artifact=artifact, app_id=app_id, device=device, server_url=server_url
|
|
638
|
+
)
|
|
639
|
+
else:
|
|
640
|
+
artifact = _run_ios_simulator(
|
|
641
|
+
builder,
|
|
642
|
+
prepared,
|
|
643
|
+
artifact=artifact,
|
|
644
|
+
app_id=app_id,
|
|
645
|
+
device=device,
|
|
646
|
+
server_url=server_url,
|
|
647
|
+
show_logs=show_logs,
|
|
648
|
+
)
|
|
649
|
+
except builder_mod.BuildError as exc:
|
|
650
|
+
print(f"Error: {exc}")
|
|
651
|
+
sys.exit(1)
|
|
652
|
+
if artifact is not None and not reuse:
|
|
653
|
+
fingerprint_mod.write_stamp(build_dir, fingerprint, artifact=artifact)
|
|
654
|
+
|
|
655
|
+
if show_logs and platform == "android":
|
|
656
|
+
_stream_logs_until_interrupt(platform, app_id, device)
|
|
657
|
+
|
|
658
|
+
|
|
659
|
+
def _native_fingerprint(
|
|
660
|
+
config: AppConfig,
|
|
661
|
+
platform: str,
|
|
662
|
+
builder: builder_mod.Builder,
|
|
663
|
+
*,
|
|
664
|
+
ios_sdks: tuple,
|
|
665
|
+
dev_client: bool,
|
|
666
|
+
) -> str:
|
|
667
|
+
return fingerprint_mod.compute(
|
|
668
|
+
config,
|
|
669
|
+
platform,
|
|
670
|
+
template_root=builder_mod.template_source(platform),
|
|
671
|
+
lib_root=builder.dev_lib_root,
|
|
672
|
+
ios_sdks=ios_sdks,
|
|
673
|
+
extra={"dev_client": "1" if dev_client else "0"},
|
|
674
|
+
)
|
|
675
|
+
|
|
676
|
+
|
|
677
|
+
def _run_android(
|
|
678
|
+
builder: builder_mod.Builder,
|
|
679
|
+
prepared: Optional[builder_mod.PreparedProject],
|
|
680
|
+
*,
|
|
681
|
+
artifact: Optional[Path],
|
|
682
|
+
app_id: str,
|
|
683
|
+
device: Optional[devices_mod.Device],
|
|
684
|
+
server_url: Optional[str],
|
|
685
|
+
port: int,
|
|
686
|
+
) -> Optional[Path]:
|
|
687
|
+
if device is not None:
|
|
688
|
+
# Both Gradle's install task and every adb call below honor
|
|
689
|
+
# ANDROID_SERIAL, so exporting it targets the whole run.
|
|
690
|
+
os.environ["ANDROID_SERIAL"] = device.identifier
|
|
691
|
+
if prepared is not None:
|
|
692
|
+
builder.install_android_debug(prepared)
|
|
693
|
+
artifact = builder.android_debug_apk(prepared)
|
|
694
|
+
elif artifact is not None:
|
|
695
|
+
install = subprocess.run(["adb", "install", "-r", str(artifact)], check=False)
|
|
696
|
+
if install.returncode != 0:
|
|
697
|
+
raise builder_mod.BuildError("adb install failed; run again with --rebuild.")
|
|
698
|
+
if server_url and "localhost" in server_url:
|
|
699
|
+
# Emulators and USB devices reach the host through adb's reverse tunnel.
|
|
700
|
+
subprocess.run(["adb", "reverse", f"tcp:{port}", f"tcp:{port}"], check=False, capture_output=True)
|
|
701
|
+
command = ["adb", "shell", "am", "start", "-n", f"{app_id}/.MainActivity"]
|
|
702
|
+
if server_url:
|
|
703
|
+
command += ["--es", "pn_dev_server", server_url]
|
|
704
|
+
subprocess.run(command, check=True)
|
|
705
|
+
return artifact
|
|
706
|
+
|
|
707
|
+
|
|
708
|
+
def _run_ios_simulator(
|
|
709
|
+
builder: builder_mod.Builder,
|
|
710
|
+
prepared: Optional[builder_mod.PreparedProject],
|
|
711
|
+
*,
|
|
712
|
+
artifact: Optional[Path],
|
|
713
|
+
app_id: str,
|
|
714
|
+
device: Optional[devices_mod.Device],
|
|
715
|
+
server_url: Optional[str],
|
|
716
|
+
show_logs: bool,
|
|
717
|
+
) -> Optional[Path]:
|
|
718
|
+
if prepared is not None:
|
|
719
|
+
artifact = builder.build_ios_simulator(prepared)
|
|
720
|
+
if artifact is None:
|
|
721
|
+
raise builder_mod.BuildError("No simulator build to install; run again with --rebuild.")
|
|
722
|
+
udid = device.identifier if device is not None else _select_ios_simulator()
|
|
723
|
+
if udid is None:
|
|
724
|
+
print("No available iOS Simulators found; open the project in Xcode to run.")
|
|
725
|
+
return artifact
|
|
726
|
+
subprocess.run(["xcrun", "simctl", "boot", udid], check=False, capture_output=True)
|
|
727
|
+
subprocess.run(["xcrun", "simctl", "install", udid, str(artifact)], check=False)
|
|
728
|
+
env = {**os.environ, "SIMCTL_CHILD_PYTHONUNBUFFERED": "1"}
|
|
729
|
+
if server_url:
|
|
730
|
+
env["SIMCTL_CHILD_PN_DEV_SERVER"] = server_url
|
|
731
|
+
command = ["xcrun", "simctl", "launch", "--terminate-running-process"]
|
|
732
|
+
if show_logs:
|
|
733
|
+
# A console PTY streams Python's stdout here; the launch blocks.
|
|
734
|
+
command.append("--console-pty")
|
|
735
|
+
command += [udid, app_id]
|
|
736
|
+
if not show_logs:
|
|
737
|
+
subprocess.run(command, env=env, check=False)
|
|
738
|
+
print("Launched iOS app on Simulator.")
|
|
739
|
+
return artifact
|
|
740
|
+
print("Launched iOS app on Simulator. Streaming logs (Ctrl+C to stop)...")
|
|
741
|
+
try:
|
|
742
|
+
subprocess.run(command, env=env, check=False)
|
|
743
|
+
except KeyboardInterrupt:
|
|
744
|
+
print()
|
|
745
|
+
subprocess.run(["xcrun", "simctl", "terminate", udid, app_id], check=False, capture_output=True)
|
|
746
|
+
print("Stopped log streaming.")
|
|
747
|
+
return artifact
|
|
748
|
+
|
|
749
|
+
|
|
750
|
+
def _run_ios_device(
|
|
751
|
+
builder: builder_mod.Builder,
|
|
752
|
+
prepared: Optional[builder_mod.PreparedProject],
|
|
753
|
+
*,
|
|
754
|
+
artifact: Optional[Path],
|
|
755
|
+
app_id: str,
|
|
756
|
+
device: devices_mod.Device,
|
|
757
|
+
server_url: Optional[str],
|
|
758
|
+
) -> Optional[Path]:
|
|
759
|
+
"""Build, install, and launch on a physical iOS device via devicectl."""
|
|
760
|
+
if prepared is not None:
|
|
761
|
+
artifact = builder.build_ios_device(prepared)
|
|
762
|
+
if artifact is None:
|
|
763
|
+
raise builder_mod.BuildError("No device build to install; run again with --rebuild.")
|
|
764
|
+
print(f"Installing on {device.name}...")
|
|
765
|
+
install = subprocess.run(
|
|
766
|
+
["xcrun", "devicectl", "device", "install", "app", "--device", device.identifier, str(artifact)],
|
|
767
|
+
check=False,
|
|
768
|
+
)
|
|
769
|
+
if install.returncode != 0:
|
|
770
|
+
print(
|
|
771
|
+
"Error: install failed. Make sure the device is unlocked, paired with this Mac, "
|
|
772
|
+
"and has Developer Mode enabled (Settings > Privacy & Security > Developer Mode)."
|
|
773
|
+
)
|
|
774
|
+
sys.exit(1)
|
|
775
|
+
command = ["xcrun", "devicectl", "device", "process", "launch", "--terminate-existing"]
|
|
776
|
+
if server_url:
|
|
777
|
+
command += ["--environment-variables", json.dumps({"PN_DEV_SERVER": server_url})]
|
|
778
|
+
command += ["--device", device.identifier, app_id]
|
|
779
|
+
launch = subprocess.run(command, check=False)
|
|
780
|
+
if launch.returncode != 0:
|
|
781
|
+
print("Error: launch failed. Launch the app from the home screen to see details.")
|
|
782
|
+
sys.exit(1)
|
|
783
|
+
print(f"Launched on {device.name}. Logs stream to the 'pn start' terminal (or Console.app).")
|
|
784
|
+
return artifact
|
|
785
|
+
|
|
786
|
+
|
|
787
|
+
def _stream_logs_until_interrupt(platform: str, app_id: str, device: Optional[devices_mod.Device]) -> None:
|
|
788
|
+
"""Tail the app's stdout on a simulator/emulator until Ctrl+C."""
|
|
789
|
+
if platform == "android":
|
|
790
|
+
proc = _start_android_log_stream()
|
|
791
|
+
else:
|
|
792
|
+
udid = device.identifier if device is not None else None
|
|
793
|
+
proc = _start_ios_log_stream(app_id, udid=udid)
|
|
794
|
+
if proc is None:
|
|
795
|
+
return
|
|
796
|
+
try:
|
|
797
|
+
proc.wait()
|
|
798
|
+
except KeyboardInterrupt:
|
|
799
|
+
print()
|
|
800
|
+
_terminate_subprocess(proc)
|
|
801
|
+
print("Stopped log streaming.")
|
|
802
|
+
|
|
803
|
+
|
|
804
|
+
# ======================================================================
|
|
805
|
+
# build
|
|
806
|
+
# ======================================================================
|
|
807
|
+
|
|
808
|
+
|
|
809
|
+
def build_project(args: argparse.Namespace) -> None:
|
|
810
|
+
"""Build standalone, distributable artifacts for ``platform``.
|
|
811
|
+
|
|
812
|
+
Args:
|
|
813
|
+
args: Parsed namespace (``platform``, ``debug``, ``upload``).
|
|
814
|
+
"""
|
|
815
|
+
platform: str = args.platform
|
|
816
|
+
debug: bool = getattr(args, "debug", False)
|
|
817
|
+
upload: bool = getattr(args, "upload", False)
|
|
818
|
+
|
|
819
|
+
config = _load_config_or_exit()
|
|
820
|
+
builder = builder_mod.Builder(config, log=print)
|
|
821
|
+
|
|
822
|
+
if upload and (platform != "ios" or debug):
|
|
823
|
+
print("Error: --upload applies to 'pn build ios' release builds only.")
|
|
824
|
+
sys.exit(1)
|
|
825
|
+
if upload and config.ios.signing.export_method != "app-store":
|
|
826
|
+
print('Error: --upload requires [ios.signing] export_method = "app-store" in pythonnative.toml.')
|
|
827
|
+
sys.exit(1)
|
|
828
|
+
|
|
829
|
+
try:
|
|
830
|
+
prepared = builder.prepare(
|
|
831
|
+
platform,
|
|
832
|
+
release=not debug,
|
|
833
|
+
ios_sdks=("iphonesimulator",) if debug else ("iphoneos",),
|
|
834
|
+
)
|
|
835
|
+
if platform == "android":
|
|
836
|
+
artifacts = builder.build_android(prepared, debug=debug)
|
|
837
|
+
else:
|
|
838
|
+
if debug:
|
|
839
|
+
app_path = builder.build_ios_simulator(prepared)
|
|
840
|
+
artifacts = builder_mod.BuildArtifacts(paths=[app_path])
|
|
841
|
+
else:
|
|
842
|
+
artifacts = builder.build_ios_archive(prepared, upload=upload)
|
|
843
|
+
except builder_mod.BuildError as exc:
|
|
844
|
+
print(f"Error: {exc}")
|
|
845
|
+
sys.exit(1)
|
|
846
|
+
|
|
847
|
+
if upload:
|
|
848
|
+
print("\nUploaded to App Store Connect. Check the build's status at appstoreconnect.apple.com.")
|
|
849
|
+
if not artifacts.paths:
|
|
850
|
+
if not upload:
|
|
851
|
+
print("Build completed, but no artifacts were found. Check the build output above.")
|
|
852
|
+
return
|
|
853
|
+
print("\nBuilt artifacts:")
|
|
854
|
+
for path in artifacts.paths:
|
|
855
|
+
print(f" {path}")
|
|
856
|
+
|
|
857
|
+
|
|
858
|
+
# ======================================================================
|
|
859
|
+
# logs
|
|
860
|
+
# ======================================================================
|
|
861
|
+
|
|
862
|
+
|
|
863
|
+
def logs_command(args: argparse.Namespace) -> None:
|
|
864
|
+
"""Stream logs from the running app without rebuilding.
|
|
865
|
+
|
|
866
|
+
Args:
|
|
867
|
+
args: Parsed namespace (``platform``, ``device``).
|
|
868
|
+
"""
|
|
869
|
+
platform: str = args.platform
|
|
870
|
+
device = _resolve_device(platform, getattr(args, "device", None))
|
|
871
|
+
if platform == "android":
|
|
872
|
+
if device is not None:
|
|
873
|
+
# Both adb and logcat below honor ANDROID_SERIAL, so exporting
|
|
874
|
+
# it targets the whole log stream at the chosen device.
|
|
875
|
+
os.environ["ANDROID_SERIAL"] = device.identifier
|
|
876
|
+
proc = _start_android_log_stream()
|
|
877
|
+
if proc is None:
|
|
878
|
+
sys.exit(1)
|
|
879
|
+
try:
|
|
880
|
+
proc.wait()
|
|
881
|
+
except KeyboardInterrupt:
|
|
882
|
+
print()
|
|
883
|
+
_terminate_subprocess(proc)
|
|
884
|
+
print("Stopped log streaming.")
|
|
885
|
+
return
|
|
886
|
+
|
|
887
|
+
# iOS: relaunch the app on the booted simulator with a console PTY so
|
|
888
|
+
# Python's stdout/stderr stream to this terminal.
|
|
889
|
+
if device is not None and device.kind == "device":
|
|
890
|
+
print("For a physical device, use Console.app or Xcode > Devices and Simulators.")
|
|
891
|
+
sys.exit(1)
|
|
892
|
+
config = _load_config_or_exit()
|
|
893
|
+
udid = device.identifier if device is not None else None
|
|
894
|
+
proc = _start_ios_log_stream(config.bundle_id, udid=udid)
|
|
895
|
+
if proc is None:
|
|
896
|
+
print("For a physical device, use Console.app or Xcode > Devices and Simulators.")
|
|
897
|
+
sys.exit(1)
|
|
898
|
+
try:
|
|
899
|
+
proc.wait()
|
|
900
|
+
except KeyboardInterrupt:
|
|
901
|
+
print()
|
|
902
|
+
_terminate_subprocess(proc)
|
|
903
|
+
print("Stopped log streaming.")
|
|
904
|
+
|
|
905
|
+
|
|
906
|
+
# ======================================================================
|
|
907
|
+
# clean
|
|
908
|
+
# ======================================================================
|
|
909
|
+
|
|
910
|
+
|
|
911
|
+
def clean_project(args: argparse.Namespace) -> None:
|
|
912
|
+
"""Remove the local ``build/`` directory.
|
|
913
|
+
|
|
914
|
+
Args:
|
|
915
|
+
args: Parsed namespace (unused).
|
|
916
|
+
"""
|
|
917
|
+
build_dir = Path.cwd() / "build"
|
|
918
|
+
if build_dir.exists():
|
|
919
|
+
shutil.rmtree(build_dir)
|
|
920
|
+
print("Removed build/ directory.")
|
|
921
|
+
else:
|
|
922
|
+
print("No build/ directory to remove.")
|
|
923
|
+
|
|
924
|
+
|
|
925
|
+
# ======================================================================
|
|
926
|
+
# Config helpers
|
|
927
|
+
# ======================================================================
|
|
928
|
+
|
|
929
|
+
|
|
930
|
+
def _load_config_or_exit(project_dir: Optional[Path] = None) -> AppConfig:
|
|
931
|
+
try:
|
|
932
|
+
return AppConfig.load(project_dir or Path.cwd())
|
|
933
|
+
except ConfigError as exc:
|
|
934
|
+
print(f"Error: {exc}")
|
|
935
|
+
sys.exit(1)
|
|
936
|
+
|
|
937
|
+
|
|
938
|
+
# ======================================================================
|
|
939
|
+
# Device log streaming
|
|
940
|
+
# ======================================================================
|
|
941
|
+
|
|
942
|
+
|
|
943
|
+
def _start_android_log_stream() -> Optional[subprocess.Popen]:
|
|
944
|
+
"""Clear logcat and stream Python-relevant tags to the terminal.
|
|
945
|
+
|
|
946
|
+
Returns:
|
|
947
|
+
The ``adb logcat`` process, or ``None`` if ``adb`` is missing.
|
|
948
|
+
"""
|
|
949
|
+
try:
|
|
950
|
+
subprocess.run(["adb", "logcat", "-c"], check=False, capture_output=True)
|
|
951
|
+
except FileNotFoundError:
|
|
952
|
+
print("Note: 'adb' not found on PATH; skipping log streaming.")
|
|
953
|
+
return None
|
|
954
|
+
try:
|
|
955
|
+
proc = subprocess.Popen(["adb", "logcat", *collect_logcat_filters()])
|
|
956
|
+
except FileNotFoundError:
|
|
957
|
+
return None
|
|
958
|
+
print("Streaming Python logs from device (Ctrl+C to stop)...")
|
|
959
|
+
return proc
|
|
960
|
+
|
|
961
|
+
|
|
962
|
+
def _booted_ios_udid() -> Optional[str]:
|
|
963
|
+
"""Return a booted iOS Simulator's UDID, or ``None`` if none is booted."""
|
|
964
|
+
try:
|
|
965
|
+
result = subprocess.run(
|
|
966
|
+
["xcrun", "simctl", "list", "devices", "booted", "--json"],
|
|
967
|
+
check=False,
|
|
968
|
+
capture_output=True,
|
|
969
|
+
text=True,
|
|
970
|
+
)
|
|
971
|
+
except FileNotFoundError:
|
|
972
|
+
return None
|
|
973
|
+
try:
|
|
974
|
+
data = json.loads(result.stdout or "{}")
|
|
975
|
+
except json.JSONDecodeError:
|
|
976
|
+
return None
|
|
977
|
+
for _runtime, devices in (data.get("devices") or {}).items():
|
|
978
|
+
for device in devices or []:
|
|
979
|
+
if device.get("state") == "Booted" and device.get("udid"):
|
|
980
|
+
return str(device["udid"])
|
|
981
|
+
return None
|
|
982
|
+
|
|
983
|
+
|
|
984
|
+
def _select_ios_simulator() -> Optional[str]:
|
|
985
|
+
"""Return a simulator UDID to target (booted first, else an iPhone)."""
|
|
986
|
+
booted = _booted_ios_udid()
|
|
987
|
+
if booted:
|
|
988
|
+
return booted
|
|
989
|
+
try:
|
|
990
|
+
result = subprocess.run(
|
|
991
|
+
["xcrun", "simctl", "list", "devices", "available", "--json"],
|
|
992
|
+
check=False,
|
|
993
|
+
capture_output=True,
|
|
994
|
+
text=True,
|
|
995
|
+
)
|
|
996
|
+
except FileNotFoundError:
|
|
997
|
+
return None
|
|
998
|
+
try:
|
|
999
|
+
data = json.loads(result.stdout or "{}")
|
|
1000
|
+
except json.JSONDecodeError:
|
|
1001
|
+
return None
|
|
1002
|
+
devices: List[Dict[str, Any]] = [d for lst in (data.get("devices") or {}).values() for d in (lst or [])]
|
|
1003
|
+
for device in devices:
|
|
1004
|
+
if "iphone 15" in (device.get("name") or "").lower() and device.get("isAvailable"):
|
|
1005
|
+
return device.get("udid")
|
|
1006
|
+
for device in devices:
|
|
1007
|
+
if device.get("isAvailable") and (device.get("name") or "").lower().startswith("iphone"):
|
|
1008
|
+
return device.get("udid")
|
|
1009
|
+
return None
|
|
1010
|
+
|
|
1011
|
+
|
|
1012
|
+
def _start_ios_log_stream(bundle_id: str, *, udid: Optional[str] = None) -> Optional[subprocess.Popen]:
|
|
1013
|
+
"""Re-launch the iOS app with a console PTY so its stdio streams here.
|
|
1014
|
+
|
|
1015
|
+
Args:
|
|
1016
|
+
bundle_id: The app's bundle identifier.
|
|
1017
|
+
udid: A specific simulator UDID to target. Falls back to the
|
|
1018
|
+
booted simulator when not given.
|
|
1019
|
+
|
|
1020
|
+
Returns:
|
|
1021
|
+
The launched process, or ``None`` when no simulator is booted.
|
|
1022
|
+
"""
|
|
1023
|
+
if udid is None:
|
|
1024
|
+
udid = _booted_ios_udid()
|
|
1025
|
+
if udid is None:
|
|
1026
|
+
print("Note: no booted iOS Simulator found; skipping log streaming.")
|
|
1027
|
+
return None
|
|
1028
|
+
env = {**os.environ, "SIMCTL_CHILD_PYTHONUNBUFFERED": "1"}
|
|
1029
|
+
try:
|
|
1030
|
+
proc = subprocess.Popen(
|
|
1031
|
+
["xcrun", "simctl", "launch", "--console-pty", "--terminate-running-process", udid, bundle_id],
|
|
1032
|
+
env=env,
|
|
1033
|
+
)
|
|
1034
|
+
except FileNotFoundError:
|
|
1035
|
+
print("Note: 'xcrun' not found on PATH; skipping iOS log streaming.")
|
|
1036
|
+
return None
|
|
1037
|
+
print("Streaming iOS app logs from the simulator (Ctrl+C to stop)...")
|
|
1038
|
+
return proc
|
|
1039
|
+
|
|
1040
|
+
|
|
1041
|
+
def _terminate_subprocess(proc: Optional[subprocess.Popen]) -> None:
|
|
1042
|
+
"""Politely stop a subprocess, escalating to ``SIGKILL`` if needed."""
|
|
1043
|
+
if proc is None or proc.poll() is not None:
|
|
1044
|
+
return
|
|
1045
|
+
proc.terminate()
|
|
1046
|
+
try:
|
|
1047
|
+
proc.wait(timeout=3)
|
|
1048
|
+
except subprocess.TimeoutExpired:
|
|
1049
|
+
proc.kill()
|
|
1050
|
+
|
|
1051
|
+
|
|
1052
|
+
# ======================================================================
|
|
1053
|
+
# Argument parsing
|
|
1054
|
+
# ======================================================================
|
|
1055
|
+
|
|
1056
|
+
|
|
1057
|
+
def codegen_command(args: argparse.Namespace) -> None:
|
|
1058
|
+
"""Generate native contracts after importing extension schema modules."""
|
|
1059
|
+
import importlib
|
|
1060
|
+
|
|
1061
|
+
from ..sdk.codegen import generate
|
|
1062
|
+
|
|
1063
|
+
for name in args.module:
|
|
1064
|
+
importlib.import_module(name)
|
|
1065
|
+
for path in generate(args.output):
|
|
1066
|
+
print(path)
|
|
1067
|
+
|
|
1068
|
+
|
|
1069
|
+
def _build_parser() -> argparse.ArgumentParser:
|
|
1070
|
+
parser = argparse.ArgumentParser(prog="pn", description="PythonNative CLI")
|
|
1071
|
+
parser.add_argument(
|
|
1072
|
+
"--version",
|
|
1073
|
+
"-V",
|
|
1074
|
+
action="version",
|
|
1075
|
+
version=f"pn {pkg_version('pythonnative')}",
|
|
1076
|
+
)
|
|
1077
|
+
subparsers = parser.add_subparsers()
|
|
1078
|
+
|
|
1079
|
+
parser_codegen = subparsers.add_parser("codegen", help="Generate typed native contracts")
|
|
1080
|
+
parser_codegen.add_argument("--output", type=Path, default=Path("generated"))
|
|
1081
|
+
parser_codegen.add_argument(
|
|
1082
|
+
"--module", action="append", default=[], help="Import a module declaring extension schemas"
|
|
1083
|
+
)
|
|
1084
|
+
parser_codegen.set_defaults(func=codegen_command)
|
|
1085
|
+
|
|
1086
|
+
parser_init = subparsers.add_parser("init", help="Scaffold a new project")
|
|
1087
|
+
parser_init.add_argument(
|
|
1088
|
+
"name",
|
|
1089
|
+
nargs="?",
|
|
1090
|
+
help="Project name, matching ^[a-z][a-z0-9_-]*$; creates ./<name>/ (default: current directory)",
|
|
1091
|
+
)
|
|
1092
|
+
parser_init.add_argument("--force", action="store_true", help="Overwrite existing files or a non-empty directory")
|
|
1093
|
+
parser_init.set_defaults(func=init_project)
|
|
1094
|
+
|
|
1095
|
+
parser_doctor = subparsers.add_parser("doctor", help="Diagnose the local toolchain and config")
|
|
1096
|
+
parser_doctor.add_argument("platform", nargs="?", choices=["android", "ios"], help="Restrict checks to a platform")
|
|
1097
|
+
parser_doctor.set_defaults(func=doctor_command)
|
|
1098
|
+
|
|
1099
|
+
parser_deps = subparsers.add_parser(
|
|
1100
|
+
"deps", help="Check which wheels [requirements].packages resolve to on each device target"
|
|
1101
|
+
)
|
|
1102
|
+
parser_deps.add_argument("platform", nargs="?", choices=["android", "ios"], help="Restrict to a platform")
|
|
1103
|
+
parser_deps.add_argument("--json", action="store_true", help="Print a JSON report for scripting")
|
|
1104
|
+
parser_deps.add_argument(
|
|
1105
|
+
"--python",
|
|
1106
|
+
help="Interpreter to run pip with (default: the one running pn; any version works, pip cross-resolves)",
|
|
1107
|
+
)
|
|
1108
|
+
parser_deps.add_argument(
|
|
1109
|
+
"--lock",
|
|
1110
|
+
action="store_true",
|
|
1111
|
+
help="Lock exact wheel versions and hashes, including both iOS Simulator architectures",
|
|
1112
|
+
)
|
|
1113
|
+
parser_deps.set_defaults(func=deps_command)
|
|
1114
|
+
|
|
1115
|
+
def _add_server_args(sub: argparse.ArgumentParser) -> None:
|
|
1116
|
+
sub.add_argument(
|
|
1117
|
+
"entry",
|
|
1118
|
+
nargs="?",
|
|
1119
|
+
help="Entry module (e.g. app.main); defaults to the project entry point",
|
|
1120
|
+
)
|
|
1121
|
+
sub.add_argument(
|
|
1122
|
+
"--port", type=int, default=DEFAULT_DEV_PORT, help=f"Port to listen on (default: {DEFAULT_DEV_PORT})"
|
|
1123
|
+
)
|
|
1124
|
+
sub.add_argument("--host", default="0.0.0.0", help="Bind address (default: 0.0.0.0 so devices can connect)")
|
|
1125
|
+
|
|
1126
|
+
parser_start = subparsers.add_parser(
|
|
1127
|
+
"start", help="Run the dev server: browser preview + Fast Refresh for every connected debug build"
|
|
1128
|
+
)
|
|
1129
|
+
_add_server_args(parser_start)
|
|
1130
|
+
parser_start.add_argument("--open", action="store_true", help="Also open the browser preview")
|
|
1131
|
+
parser_start.set_defaults(func=start_command)
|
|
1132
|
+
|
|
1133
|
+
parser_preview = subparsers.add_parser("preview", help="Run the dev server and open the browser preview")
|
|
1134
|
+
_add_server_args(parser_preview)
|
|
1135
|
+
parser_preview.add_argument("--no-open", action="store_true", help="Don't open the browser automatically")
|
|
1136
|
+
parser_preview.set_defaults(func=preview_command)
|
|
1137
|
+
|
|
1138
|
+
parser_devices = subparsers.add_parser("devices", help="List devices, emulators, and simulators")
|
|
1139
|
+
parser_devices.add_argument("platform", nargs="?", choices=["android", "ios"], help="Restrict to a platform")
|
|
1140
|
+
parser_devices.add_argument(
|
|
1141
|
+
"--json", action="store_true", help="Print a JSON array to stdout for scripting (hints go to stderr)"
|
|
1142
|
+
)
|
|
1143
|
+
parser_devices.set_defaults(func=devices_command)
|
|
1144
|
+
|
|
1145
|
+
parser_run = subparsers.add_parser("run", help="Build, install, and launch on a device/simulator")
|
|
1146
|
+
parser_run.add_argument("platform", choices=["android", "ios"])
|
|
1147
|
+
parser_run.add_argument(
|
|
1148
|
+
"--device",
|
|
1149
|
+
"-d",
|
|
1150
|
+
help="Target device: an identifier or name from 'pn devices' "
|
|
1151
|
+
"(physical iOS devices need [ios].development_team)",
|
|
1152
|
+
)
|
|
1153
|
+
parser_run.add_argument("--prepare-only", action="store_true", help="Stage + configure without building")
|
|
1154
|
+
parser_run.add_argument("--no-logs", action="store_true", help="Don't stream device logs after launch")
|
|
1155
|
+
parser_run.add_argument(
|
|
1156
|
+
"--rebuild", action="store_true", help="Run the native toolchain even when no native input changed"
|
|
1157
|
+
)
|
|
1158
|
+
parser_run.add_argument(
|
|
1159
|
+
"--dev-server",
|
|
1160
|
+
help="Dev server WebSocket URL for the app (default: the 'pn start' found on --port, via localhost/LAN)",
|
|
1161
|
+
)
|
|
1162
|
+
parser_run.add_argument(
|
|
1163
|
+
"--port", type=int, default=DEFAULT_DEV_PORT, help=f"Port 'pn start' listens on (default: {DEFAULT_DEV_PORT})"
|
|
1164
|
+
)
|
|
1165
|
+
parser_run.add_argument(
|
|
1166
|
+
"--dev-client",
|
|
1167
|
+
action="store_true",
|
|
1168
|
+
help="Build a dev client: a shell app that shows a connect screen and loads the app from any dev server",
|
|
1169
|
+
)
|
|
1170
|
+
parser_run.set_defaults(func=run_project)
|
|
1171
|
+
|
|
1172
|
+
parser_logs = subparsers.add_parser("logs", help="Stream logs from the running app")
|
|
1173
|
+
parser_logs.add_argument("platform", choices=["android", "ios"])
|
|
1174
|
+
parser_logs.add_argument(
|
|
1175
|
+
"--device",
|
|
1176
|
+
"-d",
|
|
1177
|
+
help="Target device: an identifier or name from 'pn devices' "
|
|
1178
|
+
"(physical iOS devices aren't supported for log streaming)",
|
|
1179
|
+
)
|
|
1180
|
+
parser_logs.set_defaults(func=logs_command)
|
|
1181
|
+
|
|
1182
|
+
parser_build = subparsers.add_parser("build", help="Build distributable artifacts")
|
|
1183
|
+
parser_build.add_argument("platform", choices=["android", "ios"])
|
|
1184
|
+
parser_build.add_argument("--debug", action="store_true", help="Build the debug variant instead of release")
|
|
1185
|
+
parser_build.add_argument(
|
|
1186
|
+
"--upload",
|
|
1187
|
+
action="store_true",
|
|
1188
|
+
help='Upload the iOS release build to App Store Connect (needs export_method = "app-store")',
|
|
1189
|
+
)
|
|
1190
|
+
parser_build.set_defaults(func=build_project)
|
|
1191
|
+
|
|
1192
|
+
parser_app_id = subparsers.add_parser("app-id", help="Print the resolved application/bundle id")
|
|
1193
|
+
parser_app_id.add_argument("platform", choices=["android", "ios"])
|
|
1194
|
+
parser_app_id.set_defaults(func=app_id_command)
|
|
1195
|
+
|
|
1196
|
+
parser_clean = subparsers.add_parser("clean", help="Remove the local build/ directory")
|
|
1197
|
+
parser_clean.set_defaults(func=clean_project)
|
|
1198
|
+
|
|
1199
|
+
return parser
|
|
1200
|
+
|
|
1201
|
+
|
|
1202
|
+
def main() -> None:
|
|
1203
|
+
"""Entry point for the ``pn`` console script."""
|
|
1204
|
+
parser = _build_parser()
|
|
1205
|
+
args = parser.parse_args()
|
|
1206
|
+
func = getattr(args, "func", None)
|
|
1207
|
+
if func is None:
|
|
1208
|
+
parser.print_help()
|
|
1209
|
+
sys.exit(1)
|
|
1210
|
+
func(args)
|
|
1211
|
+
|
|
1212
|
+
|
|
1213
|
+
if __name__ == "__main__":
|
|
1214
|
+
main()
|