beeware / Python-Apple-support

A meta-package for building a version of Python that can be embedded into a macOS, iOS, tvOS or watchOS project.
MIT License
1.11k stars 160 forks source link

Apple Watch WatchOS #87

Closed mobile-appz closed 2 years ago

mobile-appz commented 4 years ago

Hi has anyone got this to work successfully with WatchOS and been able to run any python code on the Apple Watch or Simulator? How do you add the extracted “watchOS” pre built framework files (from Tar archive) to an existing watchOS project as a framework? Where do you put these files? Is there any way it could be added to cocoapods or Carthage? Have also tried building with the “make watchOS” command on various versions including latest but errors occur, using latest version of XCode and MacOS. Many thanks.

freakboy3742 commented 4 years ago

Unfortunately, I can't currently point at anyone that has a working watchOS app. The underlying problem is that watchOS apps have a slightly different execution structure to macOS, iOS and tvOS apps. This means the mechanism for integrating the Python runtime into watchOS is slightly different to the other Apple platforms.

While we'd love to support watchOS formally, it's currently lower on the priority list compared to other platforms at present; we'd welcome any assistance, but you'll probably need to have prior experience with either watchOS programming or embedding Python to get any serious traction on the project.

mobile-appz commented 4 years ago

Unfortunately, I can't currently point at anyone that has a working watchOS app. The underlying problem is that watchOS apps have a slightly different execution structure to macOS, iOS and tvOS apps. This means the mechanism for integrating the Python runtime into watchOS is slightly different to the other Apple platforms.

While we'd love to support watchOS formally, it's currently lower on the priority list compared to other platforms at present; we'd welcome any assistance, but you'll probably need to have prior experience with either watchOS programming or embedding Python to get any serious traction on the project.

Thanks very much for the info. Will look in to this further and let you know if I have any luck with getting it to work.

mobile-appz commented 4 years ago

@freakboy3742 Hi just to let you know it’s working on Apple Watch series 3. Many thanks.

freakboy3742 commented 4 years ago

That's great to hear, but what does "working" mean in this context? Does this mean you've got a working WatchOS app? Can you share any details about how you did that integration (and in particular, where you've instantiated the Python VM?)

mobile-appz commented 4 years ago

That's great to hear, but what does "working" mean in this context? Does this mean you've got a working WatchOS app? Can you share any details about how you did that integration (and in particular, where you've instantiated the Python VM?)

Hi @freakboy3742 , yes have a basic Swift Apple Watch app working on the simulator and the actual Apple Watch 3 physical device which prints out "hello world" to the Xcode console. It appears that the "main.m" file which is present in iOS apps isn't present or exectuted in Apple Watch apps.

Have created a pythonStart() method in the willActivate() method found in the InterfaceController.Swift file. willActivate() is called when watch view controller is about to be visible to user. Have set up a bridging header to an objective C class which python is instantiated in, by calling the pythonStart() method. There is probably a better place to instantiate Python but at this stage was just keen to get something running.

In order to get it to work, on the physical watch had to do the following in Xcode:

In build settings, Set Valid architecture in the "Watchkit extension" target to "armv7k" for it work on the watch and set it to "$(ARCHS_STANDARD)" for it to work on the simulator. At the moment, this needs to be changed each time you want to run it on the device instead of the simulator and vice versa.

In build settings, Add the header, library and framework search paths for the "Watchkit extension" to $(PROJECT_DIR) set as recursive. This was previously causing errors where Xcode wasn't finding the Python header files it needed before this step, and which caused it not to recognise python.

In build phases, add the "Refresh Python Source" new run script phase for the "Watchkit extension" target. Copy over the script from the example projects, ensure the path to the bundle package is correct to reflect the bundle package name of the watchkit app.

Copy over the framework files from https://briefcase-support.org/python?platform=watchOS&version=3.8 to the support directory of the app.

In build settings, in the Watchkit Extension target, the above framework files need to be linked. In the Link binaries with libraries section, browse to the support directory with the libraries, and add the .a file for each library, eg libPython.a, and the others. Also link libsqlite3.tbd and libz.tbd from the default folder.

Include the Python header using "#include " in objective C. It should now be ready to be instantiated in the interfaceController.m file in an Objective C project or the InterfaceController.swift file of a swift project. The python files can be added roughly as per structure stated in https://github.com/beeware/Python-iOS-template/tree/3.7

iOS/ app/ my-project/ init.py app.py (declares PythonAppDelegate) app_packages/ ... myproject/ ... My Project.xcodeproj/ ... support/ ...

The key to get this working seemed to be setting up the Xcode "Build Settings" and "Build Phases" configuration correctly. Gather the processor architecture has changed from the Apple Watch 3, which I used, to later models so there could potentially be some issues surrounding that if people are using a more recent Apple Watch. I have been experimenting with various python versions and more recent ones seem to be able to run Python libraries better. At the moment there are some I/O permission difficulties accessing the storage on the physical watch, and also, experiencing errors with internet connectivity from Python on the physical watch, although that is working on the simulator.

Hope this helps, many thanks.

freakboy3742 commented 4 years ago

Hi @freakboy3742 , yes have a basic Swift Apple Watch app working on the simulator and the actual Apple Watch 3 physical device which prints out "hello world" to the Xcode console.

🎉

It appears that the "main.m" file which is present in iOS apps isn't present or exectuted in Apple Watch apps.

Yes - this is the crux of the problem that @meichthys hit.

Have created a pythonStart() method in the willActivate() method found in the InterfaceController.Swift file. willActivate() is called when watch view controller is about to be visible to user.

Ok - that's seems reasonable; but willActivate() will be invoked every time the watch comes into the foreground - correct? That means the Python interpreter (and interpreter state) is reset every time the watch app comes to the foreground.

Can the interpreter state be stored as an attribute of the InterfaceController, and then only initialized on the first run through? A good test of this would be to put a stateful counter in your Python code, and print(f"Hello world {n}") in the willActivate portion, so you can see that there is stateful retention between executions. To do this, it might be necessary to differentiate between "Python code that runs on startup" and "Python code that runs on activation".

In order to get it to work, on the physical watch had to do the following in Xcode:

In build settings, Set Valid architecture in the "Watchkit extension" target to "armv7k" for it work on the watch and set it to "$(ARCHS_STANDARD)" for it to work on the simulator. At the moment, this needs to be changed each time you want to run it on the device instead of the simulator and vice versa.

This intrigues me. ARCHS_STANDARD should expand to a platform appropriate setting. Do you get any error logs indicating what architecture watchOS is expecting but not getting for a hardware deployment? (i.e., what is ARCHS_STANDARD actually expanding into on physical device)

In build settings, Add the header, library and framework search paths for the "Watchkit extension" to $(PROJECT_DIR) set as recursive. This was previously causing errors where Xcode wasn't finding the Python header files it needed before this step, and which caused it not to recognise python.

I can't argue with your success, but this interests me as well. Adding the header, library and framework search paths definitely makes sense; but recursive seems unusual. What errors do you see if this is not recursive?

The key to get this working seemed to be setting up the Xcode "Build Settings" and "Build Phases" configuration correctly.

Gather the processor architecture has changed from the Apple Watch 3, which I used, to later models so there could potentially be some issues surrounding that if people are using a more recent Apple Watch.

That's definitely interesting. It's not entirely surprising that a new chip architecture has been introduced. It would also explain the need to manually change the valid architectures setting that you mentioned earlier.

A quick poke around Wikipedia suggests that Apple Watch 4 introduced a "custom 64 bit processor", which suggests that there is a new arm64 variant that we may need to add x86_64 support to the simulator binary. I also found one project on github that references an "arm64_32" architecture for watchOS; we may need to add that to the support package build.

I have been experimenting with various python versions and more recent ones seem to be able to run Python libraries better. At the moment there are some I/O permission difficulties accessing the storage on the physical watch, and also, experiencing errors with internet connectivity from Python on the physical watch, although that is working on the simulator.

Interesting. Fixing those will need some deep diving into the CPython source... good hunting :-)

mobile-appz commented 4 years ago

@freakboy3742

Hi think you’re right about the instantiation resetting when app comes to foreground, haven’t attempted to test or resolve this yet. That’s a very good idea about the counter though to test it.

Here is the Xcode error using armv7k in Valid Architectures, with app running on simulator:

error: Embedded binary is not signed with the same certificate as the parent app. Verify the embedded binary target's code sign settings match the parent app's.

    Embedded Binary Signing Certificate:    Not Code Signed
    Parent App Signing Certificate:     - (Ad Hoc Code Signed)

Here is the Xcode error using $(ARCHS_STANDARD) in Valid Architectures, with app running on physical Apple Watch:

error Must define SIZEOF_WCHAR_T

failed to emit precompiled header ‘[path_to_App]/App WatchKit Extension-Bridging-Header-swift_AYECBGWSQNLR-clang_1ZFE2KWIX7UFG.pch' for bridging header ‘[path_to_App]/App WatchKit Extension/App WatchKit Extension-Bridging-Header.h'

$(ARCHS_STANDARD) expands in Xcode to armv7k arm64_32

Guess arm64_32 support might need to be added to binary to support recent Apple Watches, had a look at all the Makefiles related to this but not exactly sure how to go about doing that or how they work.

When you mentioned the recursive thing I looked in to all the search paths again and it turned out that the app would run even without the header search path added at all, once changing the import statement in the app code to use double quotes instead of angle brackets ie #import "Python.h".

From what I found so far online it seems the internet connectivity is quite restricted on the Apple Watch to certain very limited Apple APIs so maybe some kind of bridging code would be needed for that. Is there anyway to debug the embedded python code using Xcode by using breakpoints and stepping through it during execution of the WatchOS app like you can with the objective C and Swift? For your info (in case you might have seen similar on other platforms) the errors when attempting a network connection in Python on the Physical Apple Watch are:

[Errno 65] No route to host 2020-05-24 19:14:06.429767+0100 App WatchKit Extension[369:298329] dnssd_clientstub ConnectToServer: connect() failed path:/var/run/mDNSResponder Socket:9 Err:-1 Errno:1 Operation not permitted Traceback (most recent call last): File "[/path_to_app/]App.app/PlugIns/App WatchKit Extension.appex/Library/Python/lib/python37.zip/urllib/request.py", line 1317, in do_open File "[/path_to_app/]App.app/PlugIns/App WatchKit Extension.appex/Library/Python/lib/python37.zip/http/client.py", line 1252, in request File "[/path_to_app/]App.app/PlugIns/App WatchKit Extension.appex/Library/Python/lib/python37.zip/http/client.py", line 1298, in _send_request File "[/path_to_app/]App.app/PlugIns/App WatchKit Extension.appex/Library/Python/lib/python37.zip/http/client.py", line 1247, in endheaders File "[/path_to_app/]App.app/PlugIns/App WatchKit Extension.appex/Library/Python/lib/python37.zip/http/client.py", line 1026, in _send_output File "[/path_to_app/]App.app/PlugIns/App WatchKit Extension.appex/Library/Python/lib/python37.zip/http/client.py", line 966, in send File "[/path_to_app/]App.app/PlugIns/App WatchKit Extension.appex/Library/Python/lib/python37.zip/http/client.py", line 938, in connect File "[/path_to_app/]App.app/PlugIns/App WatchKit Extension.appex/Library/Python/lib/python37.zip/socket.py", line 707, in create_connection File "[/path_to_app/]App.app/PlugIns/App WatchKit Extension.appex/Library/Python/lib/python37.zip/socket.py", line 748, in getaddrinfo socket.gaierror: [Errno 8] nodename nor servname provided, or not known

During handling of the above exception, another exception occurred:

Traceback (most recent call last): File "[/path_to_app/]App.app/PlugIns/App WatchKit Extension.appex/Library/Application Support/app.watchkitapp/app_packages/pytest.py", line 31, in s = ur.urlopen("http://www.google.com") File "[/path_to_app/]App.app/PlugIns/App WatchKit Extension.appex/Library/Python/lib/python37.zip/urllib/request.py", line 222, in urlopen File "[/path_to_app/]App.app/PlugIns/App WatchKit Extension.appex/Library/Python/lib/python37.zip/urllib/request.py", line 525, in open File "[/path_to_app/]App.app/PlugIns/App WatchKit Extension.appex/Library/Python/lib/python37.zip/urllib/request.py", line 543, in _open File "[/path_to_app/]App.app/PlugIns/App WatchKit Extension.appex/Library/Python/lib/python37.zip/urllib/request.py", line 503, in _call_chain File "[/path_to_app/]App.app/PlugIns/App WatchKit Extension.appex/Library/Python/lib/python37.zip/urllib/request.py", line 1345, in http_open File "[/path_to_app/]App.app/PlugIns/App WatchKit Extension.appex/Library/Python/lib/python37.zip/urllib/request.py", line 1319, in do_open urllib.error.URLError: <urlopen error [Errno 8] nodename nor servname provided, or not known> 2020-05-24 19:14:06.468523+0100 App WatchKit Extension[369:298329] Python initialized Loading preferences from /var/mobile/Containers/Data/PluginKitPlugin/E3BE7F42-5DF7-457A-BE70-2C10C7294511/Documents/playlists.plist [Errno 65] No route to host 2020-05-24 19:14:06.506135+0100 App WatchKit Extension[369:298329] dnssd_clientstub ConnectToServer: connect() failed path:/var/run/mDNSResponder Socket:9 Err:-1 Errno:1 Operation not permitted

Thanks for your help.

freakboy3742 commented 4 years ago

FYI: I started taking a look at the changes that would be necessary to support arm64_32; here is a branch that contains some initial updates for Python 3.8.2. It's not quite working yet - I suspect there's an additional patch needed for the Python config.sub script to define the existence of the arm64_32 architecture - but it's a start.

Once it works, you should be able to use that branch to build a local support package; if I'm right, it should address the ARCHS_STANDARD and SIZEOF_WCHAR_T issues you describe.

The other two errors, however, are more concerning - they suggest a deeper set of limitations in the capabilities of watchOS. This isn't uncommon - POSIX defines the existence of many APIs, but then they fail (either with errors, crashes, or lockups) if you actually try to use them. The fork and spawn APIs are the classic example here, but there are others. You may need to do some investigation to determine what network calls watchOS is allowed to make.