dotnet / runtime

.NET is a cross-platform runtime for cloud, mobile, desktop, and IoT apps.
https://docs.microsoft.com/dotnet/core/
MIT License
15.06k stars 4.69k forks source link

Add Mac Catalyst TargetFrameworkIdentifier #44882

Closed rolfbjarne closed 3 years ago

rolfbjarne commented 3 years ago

I'm implementing support for Mac Catalyst in Xamarin, and I've run into a question whether a new TFI should be created or not.

First a few facts:

Adding a new TFI would have a few consequences:

Not adding a new TFI would also have a few consequences (say we re-use Xamarin.iOS):

Design proposal https://github.com/dotnet/designs/pull/174/

Dotnet-GitSync-Bot commented 3 years ago

I couldn't figure out the best area label to add to this issue. If you have write-permissions please help me learn by adding exactly one area label.

marek-safar commented 3 years ago

@terrajobst @ericstj @mhutch would you be in favour of adding a new TFM for this configuration

Redth commented 3 years ago

If we didn't add a new TFI, we could probably still reason about attempting to use API's with runtime exceptions.

Given that Catalyst is much more similar to iOS, would we be able to swap out API binding references based on a project property? By default you get Xamarin.iOS.dll, but if you set <MacCatalyst>True</MacCatalyst>, we instead give you Xamarin.MacCatalyst.dll which has the superset of available API's? We could then use the existing API annotations on those bindings in roslyn analyzers to advise the user of things unavailable when building with -p:MacCatalyst=False. Then the iOS TFI could pivot based on that variable and build with the correct binding dll. We could also provide #define CATALYST based on the variable. The one disadvantage here is I'm not sure how nugets could provide anything but the MacCatalyst API superset of their assemblies to iOS/Catalyst developers. We could make the attribute available so they could proactively annotate their own API's around incompatibilities, but that feels sad.

For NuGets, this would mean we can still consume existing assemblies targeting iOS, though I'm not sure if there's a great path to surfacing runtime errors if you try and call an API path in one of these existing assemblies that isn't implemented on Catalyst, beyond some unhandled exception lacking much context.

rolfbjarne commented 3 years ago

Given that Catalyst is much more similar to iOS, would we be able to swap out API binding references based on a project property? By default you get Xamarin.iOS.dll, but if you set <MacCatalyst>True</MacCatalyst>, we instead give you Xamarin.MacCatalyst.dll which has the superset of available API's?

Would that work with the IDEs and things like intellisense? Or would you get red squiggles whenever you tried to use a Catalyst only API?

Redth commented 3 years ago

Given that Catalyst is much more similar to iOS, would we be able to swap out API binding references based on a project property? By default you get Xamarin.iOS.dll, but if you set <MacCatalyst>True</MacCatalyst>, we instead give you Xamarin.MacCatalyst.dll which has the superset of available API's?

Would that work with the IDEs and things like intellisense? Or would you get red squiggles whenever you tried to use a Catalyst only API?

I think that is the intention, if you aren’t specifying catalyst, you don’t get the APIs to reference. Or are you thinking the IDE may not handle swapping references based on a property this way very well?

rolfbjarne commented 3 years ago

Given that Catalyst is much more similar to iOS, would we be able to swap out API binding references based on a project property? By default you get Xamarin.iOS.dll, but if you set <MacCatalyst>True</MacCatalyst>, we instead give you Xamarin.MacCatalyst.dll which has the superset of available API's?

Would that work with the IDEs and things like intellisense? Or would you get red squiggles whenever you tried to use a Catalyst only API?

I think that is the intention, if you aren’t specifying catalyst, you don’t get the APIs to reference. Or are you thinking the IDE may not handle swapping references based on a property this way very well?

It's the second part: I wonder if the IDE will handle the reference swap correctly.

marek-safar commented 3 years ago

What about the following? We keep an only a single version of APIs, we could keep calling it Xamarin.iOS. We annotate all APIs which are not available on the Catalyst with [UnsupportedOSPlatformAttribute ("osx")] attribute and rely on existing platform compatibility analyzer to take care of flagging the unavailable APIs to the developers.

rolfbjarne commented 3 years ago

What about the following? We keep an only a single version of APIs, we could keep calling it Xamarin.iOS. We annotate all APIs which are not available on the Catalyst with [UnsupportedOSPlatformAttribute ("osx")] attribute and and rely on existing platform compatibility analyzer to take care of flagging the unavailable APIs to the developers.

That could work.

We'd also have to add all the macOS API (AppKit) that are available in Catalyst but not in iOS, and mark those with [UnsupportedOSPlatformAttribute ("iOS")].

and rely on existing platform compatibility analyzer

I assume there's a way to tell the platform compatibility analyzer which platform we're building for outside of the TFM?

am11 commented 3 years ago

fwiw, currently the granularity chosen for shipping artifacts is 'package', such that, there is no concept of 'very similar' when it comes to a consumable package; slightly different is just another package. it has some benefit at the cost of a slightly scaled bandwidth usage (which is not an issue in this day and age). e.g. in .net 5, the difference between dotnet-sdk-linux-x64.tar.gz (linux glibc x64) and dotnet-sdk-linux-musl-x64.tar.gz (linux musl-libc x64) is about 13 binary files; out of total of 3100+ files, yet they are kept separate as it is easy to tag and reason about, imo (and it goes hand in hand with tfm mappings on engineering side of the house).

filipnavara commented 3 years ago

If you keep a single TFM how would I actually multi-target an app for both iOS and macOS in a single build?

mhutch commented 3 years ago

@terrajobst @richlander

My initial feeling is that we need a new TFM. But catalyst doesn't seem particularly descriptive and may end up seeming out of date in a few years. Maybe we should use net6.0-mac-catalyst? Or given we haven't shipped net6.0-mac yet, maybe we should use that for Catalyst and use net6.0-mac-cocoa for Xamarin.Mac?

mhutch commented 3 years ago

We could allow net6.0-mac-catalyst to reference the legacy Xamarin.iOS and Xamarin.mac TFMs, the same way we allow net6.0-ios to reference Xamarin.iOS and allow net6.0-mac to reference Xamarin.Mac. We should not however allow net6.0-mac-catalyst to reference net6.0-ios or net6.0-mac directly; any library that is rebuilt for the net6.0+ TFMs should be expected to use multi-targeting.

When the fallback is used, we should emit a warning that some APIs may not work. We will also need type forwarders so binary assembly references continue to work. And I would strongly suggest that we have separate reference and implementation versions of the catalyst platform assemblies; the reference version should contain supported APIs, while the implementation can include Mac and iOS APIs that are not present on Catalyst and that throw NotSupportedException. This will ensure developers don't end up with weird MissingMethodException or TypeLoadException runtime errors when using legacy references. We could possibly also add link time errors for increased robustness.

spouliot commented 3 years ago

Apple defines Catalyst as a variant of iOS, not macOS (just like the simulators are seen as a variant).

E.g. an .xcframework would have a ios-x86_64-maccatalyst directory for the platform specific binaries

praeclarum commented 3 years ago

Please no, please no, please no :-)

There is no need to fork thousands of nugets. There is no need to manage yet another project and its extensions.

It will be a tremendous amount of busy work for no gains. Take a hint from Apple and keep it as simple as a checkbox.

Instead, Platform=MacCatalyst will give us everything we want.

spouliot commented 3 years ago

There are some divergences between iOS and macOS API (e.g. same type, different members). I think they favour the iOS side (but with Apple it's safer to assume we'll have a mix of both).

We must also be sure that submitted applications do not refer to any symbols not available on the target platform. Otherwise Apple could reject the binaries. As such adding macOS API to Xamarin.iOS.dll can be problematic.

filipnavara commented 3 years ago

@praeclarum How would that work to multi-target both iOS and macOS in a single build?

It's perfectly doable to have TFM net6.0-catalyst (or different name, not a fan personally) that is compatible with both net6.0-ios and xamarin.ios as superset. That would allow the NuGets to continue to work as long as they targeted Xamarin.iOS already.

praeclarum commented 3 years ago

@filipnavara You’re talking about TFMs and net6.0 that don’t work/exist today. To support a scenario literally no one does today.

It wouldn’t work in a single build and doesn’t need to.

praeclarum commented 3 years ago

@mhutch The biggest issue these apps face is compatibility with existing libraries/nugets. You know how long it took people just to get basic Xamarin.iOS in their nugets. If you do a new TFM, it will be years before libraries are recompiled, and you will making life unnecessarily difficult for app developers and library authors.

filipnavara commented 3 years ago

@praeclarum Well, this issue is about TFM for .NET 6.0. I literally use the .NET 5/6 TFMs with the prerelease packages today. I previously used MSBuild.Sdk.Extras to support the same multi-targeting scenario in a similar fashion.

I do want it to work in a single build. New TFM would allow that and it would not break any of the scenarios you mentioned or require new NuGets. In fact, adding Catalyst support would be matter of changing a single <TargetFramework>net6.0-ios</TargetFramework> line into <TargetFrameworks>net6.0-ios;net6.0-catalyst</TargetFrameworks> in the csproj.

I'm operating on the assumption that Mac Catalyst is strict superset of Xamarin.iOS. Small differences can be handled by PlatformNotSupportedExceptions if necessary. As long as the relationship exists in the native code then the same rule can apply to the NuGet TFMs. A package targeting Xamarin.iOS1.0 would be consumable by net6.0-ios or the new hypothetical net6.0-catalyst.

praeclarum commented 3 years ago

@filipnavara I’m just begging you to consider the bigger picture. People put native code in their Xamarin.iOS nugets, with your new TFM those nugets will have to change to work. That’s going to take a long time. Please consider the people that will be using this tech.

filipnavara commented 3 years ago

@praeclarum If they put native code in the NuGet it would not work anyway, would it? You would still need to recompile the native code with the correct compiler flags and architecture, right? I don't see how keeping the same TFM would help you. In fact, I can see quite the opposite of that.

(And if it would work without recompilation then, again, new TFM can be compatible, just like net6.0-ios can consume xamarin.ios1.0 NuGets)

praeclarum commented 3 years ago

It does work! So long as it’s built with Xcode 11.3+

It’s not a new architecture, it’s a new target. It’s the the x86 64 ABI.

Please consider gaining some experience writing Catalyst apps before inflicting this pain on others.

Anyway, I’m just repeating myself and will stop arguing. PLEASE just consider the community before breaking all our apps.

filipnavara commented 3 years ago

@praeclarum I was following all the PRs and the work you did on the Catalyst support. I think you are still missing the point that new TFM doesn't mean that the NuGets need to be republished to support it. Look at the design document for .NET 5 TFMs. You can have a new TFM that is compatible with a previously defined TFM. Any potential net6.0-catalyst would be able to consume NuGets with xamarin.ios TFM that were produced today, as long as the TFM is declared with the correct (backward) compatibility rules. The only thing you would need to change is the TFM of the final application which is exactly one line (in new style .csproj).

Redth commented 3 years ago

Since Catalyst is most close to iOS (it's really just missing a handful of frameworks that work on iOS but not Catalyst), could we move the AppKit API's which are supported on Catalyst into a separate NuGet package, and we leave the iOS API's which don't work annotated with attributes so analyzers from the Catalyst NuGet package would help inform usages of iOS API's that will throw at runtime when Platform==Catalyst? The Catalyst package's existence could also have analyzers to inform the opposite (when catalyst API's are used when Platform!=Catalyst).

filipnavara commented 3 years ago

It’s not a new architecture, it’s a new target. It’s the the x86 64 ABI.

That still sounds to me that the NuGets with native code would have to be rebuilt if they only ship iOS/arm64 native libraries today.

Right now you have a combination of TargetFrameworkIdentifier (TFM) and RuntimeIdentifier (RID). If the API surface was identical to iOS it would make sense to stick to the iOS TFM (net6.0-ios and xamarin.ios1.0). NuGets could then can ship libraries compiled against different ABI (x64/arm64) using different RIDs. And you could multi-target using a set of RuntimeIdentifiers in your app (the modern SDK equivalent of Platform/Platforms/PlatformTarget, if oversimplified).

If the API surface is different enough then it could warrant a new TFM. If you go with the new TFM then it should NOT require modifying every single NuGet out there. At minimum it should be compatible with whatever NuGets target Xamarin.iOS today. Additionally, if the API is superset of the iOS API I would expect it to be a superset on the TFM level as well, ie. hypothetical net6.0-ios-catalyst would be compatible with net6.0-ios and xamarin.ios1.0.

Redth commented 3 years ago

That still sounds to me that the NuGets with native code would have to be rebuilt if they only ship iOS/arm64 native libraries today.

No, most nugets that have actual native binaries in them would be fat architecture with both x86_64 and arm64 at a minimum. Nothing needs rebuilding for Catalyst if that's true (which it almost always is). Most nugets won't even have native binaries like this to begin with as well, just managed .net assemblies calling XamariniOS api's.

Redth commented 3 years ago

Existing NuGet compatibility really just boils down to whether they use any API's in Xamarin.iOS.dll that aren't compatible at runtime with Catalyst. As I mentioned, it's a handful of frameworks for the most part. We've already been taking existing iOS apps built against mono/mono runtime and running them with Catalyst with fantastic success.

Redth commented 3 years ago

Realized I failed to explain the existing x86_64 arch for those who may not know. This is what the iPhone simulator uses, so it’s rather uncommon for native libs to ship without this architecture in them or they wouldn’t work on the simulator. Catalyst apps are very very nearly iPhone simulator apps. They just have a few less iOS APIs available and a few extra ones from appkit that can optionally be used.

filipnavara commented 3 years ago

No, most nugets that have actual native binaries in them would be fat architecture with both x86_64 and arm64 at a minimum.

Fair enough, I forgot about fat binaries. But are the current NuGets targeting Xamarin.iOS published with those? Would that basically be the same binaries that are used with the iOS simulator today? (update: Thanks, got the answer above already while I was writing the comment.)

Note that I am not arguing to drop support for any existing NuGets, quite the opposite. I expect that supporting Xamarin.iOS TFMs without native code would be trivial with or without a new TFM. With native code it may be tricky but it depends on how the NuGets are laid out internally but that would also be problem whether a new TFM is introduced or not.

Existing NuGet compatibility really just boils down to whether they use any API's in Xamarin.iOS.dll that aren't compatible at runtime with Catalyst.

There are several different strategies on how to handle that. Ideally the iOS API would not be monolithic DLL and would be more like per-framework DLLs. Then you could exclude the frameworks from specific targets. Obviously this would be really difficult to pull off with proper backward compatibility (likely not impossible, just very hard). Another possibility is to expose the APIs from the missing frameworks but make them throw PlatformNotSupportedException. This would ideally be combined with some analyzer.

mhutch commented 3 years ago

Apple has the concept of a "targetEnvironment" as an additional dimension of the "target triple" compilation pivot. The API surface can differ depending on the target environment and the app can use conditional compilation (i,e, ifdefs) predicated on the environment,

The only way to do this with .NET is multi-targeting. .NET makes a distinction between the API pivots (the TFM) and the runtime pivots (the RID), and the TFM for .NET 5.0+ has intentionally been kept simple, with only three dimensions: net<netversion>-<platform><platformversion>.

I don't particularly want to add another dimension to TFMs as that would have huge complicated knock on effects, and TargetFrameworkPlatform is the existing one that's the best fit.

@praeclarum you wouldn't need more projects and forked nugets. Apps and libraries could do this:

<TargetFrameworks>net6.0-ios;net6.0-mac-catalyst</TargetFrameworks>

and my proposed back-compat would allow referencing existing NuGets.

IMO the closer we stay to how Apple does things the less likely we are run into complications in the future.

terrajobst commented 3 years ago

I'm not an expert on the Mac side, so I genuinely don't have strong feelings one way or the other.

But what I can do is give you a process by which you can decide whether or not you need a TFM.

  1. Would you like to be able to install existing packages that don't target this new TFM into a project that uses the new TFM?
    • If the answer is no, a TFM is fine.
    • If the answer is yes, a TFM isn't impossible but it's a red flag that you probably shouldn't create one.
  2. How much do the API surfaces differ between Catalyst and iOS/macOS?
    • If you expect the vast majority of iOS/macOS APIs to work in Catalyst, then I think you want to reuse the respective TFM (either macOS or iOS) and simply annotate the APIs that don't work.
    • If there is very limited API overlap, then having separate TFMs might be fine.

(1) is about desired policy, (2) is about how much the desired policy makes sense.

Stated differently, I think TFMs work great when you're modeling independent islands, for example Android vs. iOS, because there is no scenario where one needs to reference libraries from the other. TFMs are less useful when you need to model families of the same OS, because code sharing desires are usually complicated and may change over time, which often doesn't play well with TFMs because as @praeclarum outlined earlier, TFMs are very stiff b/c the ecosystem takes years to adopt them.

Assuming Catalyst is very close to a superset of an existing TFM then I'd not introduce a new TFM and simply have a single TFM and use platform annotations to allow users to write adaptive code, i.e. rely on runtime checks. It sounds a bit like as if Catalyst is a superset of macOS + iOS. If that's the case, then we're a bit screwed because reusing either TFM would still disallow the other. In that case, you have two options: declare one TFM the winner, add a compat fallback for the other, and moving forward only invest in the winner. And if you really dislike the name, you could introduce a third, deprecate the existing ones and add a fallback for both. But either way what we can't do is shipping multiple TFMs that can reference each other. The only way we can model sharing is "shared with all" (net5.0) and "shared with same TFM only" (net5.0-<os>) . Anything else is PCLs and we know it doesn't scale and is extremely complicated.

If you don't create a new TFM this would exclude multi-targeting, but I'm working on a spec to enable multi-targeting with RIDs. Regardless of your choice for TFM, Catalyst should get a separate RID. And with this feature, people would be able to write #if to provide different behaviors between them. However, the TFM choice dictates whether our not libraries can offer different API surface between macOS, iOS and Catalyst. However, library authors would still be able to indicate which of their API surface works on Catalyst/iOS and which one doesn't (via the new support attributes in .NET 5).

My gut feel (again, I'm not an expert here) is that it sounds like you may want to be able to consume existing iOS libraries, in which case I'd propose to not introduce a new TFM and simply reuse the one for iOS.

In fact, we may want to think about this scenario for all the Apple platforms. The current plan is having different TFMs for WatchOS vs iOS vs tvOS. But maybe having a joint TFM with annotations for most people, with the ability of #if with RID for advanced scenarios/native code, is a better approach?

mhutch commented 3 years ago

Yeah, if we have the ability to multi-target across RIDs and annotate APIs as only working on certain environments/devices (rather than just OSes) that expands the options.

mhutch commented 3 years ago

My gut feel (again, I'm not an expert here) is that it sounds like you may want to be able to consume existing iOS libraries, in which case I'd propose to not introduce a new TFM and simply reuse the one for iOS.

Developers may also want to consume also some existing Mac libraries, hence my earlier back-compat proposal.

Redth commented 3 years ago

My gut feel (again, I'm not an expert here) is that it sounds like you may want to be able to consume existing iOS libraries, in which case I'd propose to not introduce a new TFM and simply reuse the one for iOS.

Exactly ^

Developers may also want to consume also some existing Mac libraries, hence my earlier back-compat proposal.

It’s certainly possible but in practice I don’t anticipate that scenario to be very common. The subset of macOS appkit APIs available in Catalyst is much smaller the iOS APIs and are completely optional (you don’t need any of them to build a functional catalyst app, but they can be used to enhance your app’s experience on macos)

It does sound like RID gives us the ability to have the #define we’d want and be a way to pivot the additional macos APIs available (and help annotate the iOS ones which are not).

Redth commented 3 years ago

In fact, we may want to think about this scenario for all the Apple platforms. The current plan is having different TFMs for WatchOS vs iOS vs tvOS. But maybe having a joint TFM with annotations for most people, with the ability of #if with RID for advanced scenarios/native code, is a better approach?

The line of supported iOS APIs becomes more blurry with watchOS and tvOS but is not totally different than catalyst. Would having RID allow us to package assemblies in nuget for each RID too?

terrajobst commented 3 years ago

RIDs are already available and packages can use them for multi-targeting, but today it's rocket science to make that work. Hence, we generally don't want to build experiences that hinges on 3rd parties being able to multi-target across RIDs. However, in many ways that applies to multi-targeting in general, because the complexity jumps up from a single output by quite a bit. That's partially due to lack of tooling in the UI, but also because developers need to consider compatibility between their own multi-targeted outputs, for which we don't have any validation today. The goal of my spec is to lift multi-targeting for RIDs to a point where it's as easy (or hard) as multi-targeting between TFMs. In a sense, RIDs are simply a more restricted form of multi-targeting (can't differ API surface, can't differ dependencies, etc).

Would having RID allow us to package assemblies in nuget for each RID too?

Yes, with the restrictions I mentioned above. And we have many more RIDs than we have frameworks. Basically, we have RIDs for anything that is relevant for native assets, which is all operating system and CPU architectures. So from my point of view, having a RID for Catalyst seems to be a given.

rolfbjarne commented 3 years ago

One thing that would complicate re-using the Xamarin.iOS TFM is if Apple adds API to Catalyst that's incompatible with iOS.

I looked through the Catalyst API, and I found at least one instance that affects the API bindings: the SslCipherSuite enum is a uint on Catalyst and ushort on iOS (https://github.com/xamarin/xamarin-macios/blob/d7bb2f2d9f2d33735b69e83dd0e8fb2425673168/src/Security/SecureTransport.cs#L278-L282): https://developer.apple.com/documentation/security/sslciphersuite?language=objc

It would not surprise me in the least if Apple were to do this in more places in the future.

mhutch commented 3 years ago

One thing that would complicate re-using the Xamarin.iOS TFM is if Apple adds API to Catalyst that's incompatible with iOS.

Yes, that's exactly the kind of problem I was referring to in "IMO the closer we stay to how Apple does things the less likely we are run into complications in the future."

If we use a single TFM then we're essentially building an abstraction that may not hold up in the future,

rolfbjarne commented 3 years ago

@praeclarum

Please no, please no, please no :-)

There is no need to fork thousands of nugets. There is no need to manage yet another project and its extensions.

It will be a tremendous amount of busy work for no gains. Take a hint from Apple and keep it as simple as a checkbox.

It's only as simple as a checkbox for the actual app project. If you want to build your library project for Mac Catalyst in addition to the iOS Simulator/device, things get complicated fast. If you want to ship your library project for multiple platforms (say Mac Catalyst, iOS Simulator + iOS device), you can't even use Xcode, you have to drop down to the terminal.

People put native code in their Xamarin.iOS nugets, with your new TFM those nugets will have to change to work.

People will have to rebuild/repackage their native code anyway, it's not possible to re-use native libraries built for the iOS Simulator on Mac Catalyst.

You'll get this error if you try:

error: Building for Mac Catalyst, but the linked library 'libsimlib.a' was built for iOS Simulator.

I understand it can be painful to rebuild NuGets for yet another TFM, but at this point I'm not sure we have a choice, because as far as I understand from other people's comments, that's the only way to offer an API that's (ever so slightly for now) incompatible with Xamarin.iOS.

coolbluewater commented 3 years ago

[Edited for brevity.]

Let's note that a "Xamarin.iOS" project today includes iOS as well as iPadOS. iPadOS was introduced late in the game and I don't think there are API differences yet, but there will be in the future. Nevertheless, Xamarin.iOS has always exposed and supported options to build for iPhone, iPad, or both (Universal.) The Xamarin build system knows how to package the binaries as well as iPhone/iPad-specific assets and Info.Plist to create a universal binary.

About Mac Catalyst: According to Apple it lets you Create a version of your iPad app that users can run on a Mac device.. In other words, it is a variant of iPadOS that includes some macOS features.

It is correct and consistent with Xcode, therefore, to further extend the meaning of "Universal" to include Mac Catalyst apps. And it should be exposed via a single checkbox. That is, a single .csproj that can build an app that deploys to the iPhone and/or iPad and/or Mac Catalyst. Why would we want to make it more heavy weight than this? We shouldn't. The existing .csproj has all the information we need. We just need to add a setting for enabling Mac Catalyst.

I understand it can be painful to rebuild NuGets for yet another TFM, but at this point I'm not sure we have a choice, because as far as I understand from other people's comments, that's the only way to offer an API that's (ever so slightly for now) incompatible with Xamarin.iOS.

Is the result going to be a single csproj and a single nuget package with all the needed binaries, including separate binaries for Mac Catalyst? If that's the case then it's a detail that is invisible to the developer. On the other hand if you mean that there will be separate nugets for vanilla Xamarin.iOS and for Mac Catalyst, I don't understand why. If the API being exposed by the library is consistent across these platforms, then there shouldn't be the need for a separate nuget. We have plenty of examples of nuget libraries that have platform-specific implementations but yet offer up the same API across the platforms they support. I don't think nugets with different APIs between Xamarin.iOS and Mac Catalyst is a valid use-case.

Mogikan commented 3 years ago

It does not sound great to rebuild all nuget packages. Let's take as an example abandoned https://github.com/xamarin/SignaturePad. Owner can not push through pull requests with WPF and MacOS support and unlikely to rebuild nuget with new TFI.

dalexsoto commented 3 years ago

If the API being exposed by the library is consistent across these platforms, then there shouldn't be the need for a separate nuget.

Unfortunately this does not hold true for Apple SDKs, for example this was reported in our discord channel NSTextAlignment the value depends on the platform it is run and @rolfbjarne already pointed out above another case, it seems that the only way to safely future proof is really having a different TFI, the vast majority (if not all) the nuget packages that contain any native libraries will need to be rebuilt anyways unfortunately.

coolbluewater commented 3 years ago

@dalexsoto - that's a platform difference, not a library difference. By library, I mean the one that the developer is creating.

For concreteness, we have three "platforms" - iOS, iPadOS, and Mac Catalyst. The first two are served by the current Xamarin.iOS as noted. What does the proposed csproj look like in the case where I want to build a) a new nuget library and b) an app, that both deploy to all three "platforms"?

dalexsoto commented 3 years ago

For concreteness, we have three "platforms" - iOS, iPadOS, and Mac Catalyst.

Kind of, you are leaving away tvOS and watchOS which both have their own Xamarin.*.dll all of these platforms share some APIs that is true but tvOS and watchOS have some platform specific frameworks. iPadOS is just a marketing thing from Apple, it still identifies itself as iOS but Mac Catalyst isn't and it has kind of moved away from just a checkbox this year:

mac-idiom-selector

One of my fears is that Apple goes beyond next year and they could have a Mac Catalyst only framework or introduces Mac Catalyst platform APIs or removes already exposed API (for example) from UIKit only in the MacCatalyst context, these would pollute Xamarin.iOS.dll API surface which we could annotate but as developers already found out even if the current API is exposed it does not mean it works so nothing stops Apple on creating breaking changes. If I understand correctly by having a different TFM and a Xamarin.MacCatalyst.dll would allow us to accommodate such changes.

There is a big warning there already from their docs

Mac apps built with Mac Catalyst can only use AppKit APIs marked as available in Mac Catalyst, such as NSToolbar and NSTouchBar. Mac Catalyst doesn’t support accessing unavailable AppKit APIs.

About how csproj would look I am not completely sure, this reminds me a lot about the "Shared Projects" we used to have to share code between iOS or Android back when Nuget profiles was a thing...

@coolbluewater nice points btw :)

coolbluewater commented 3 years ago

One of my fears is that Apple goes beyond next year

@dalexsoto - why is this a problem? I think its a matter of realizing that msbuild and nuget already allow for all this variation. Nothing new needs to be invented. From the point of view of the machine, it is completely different code that runs on the simulator and device, for example. The constructs of "library", "app" and "project" are fluid and should correspond to how the developer thinks of these, or the result is cognitive dissonance.

When I think of a Xamarin.iOS project I know that there are features that are unsupported on the iPhone that are supported on the iPad, so my code adapts appropriately. It is fine even for APIs to differ in signature, such as NSTextAlignment - msbuild and nuget have no difficulty with compiling & packaging different outputs for each of the platforms that live under the umbrella of a) the same nuget library or b) the same "app" that can be built for different targets. If a build break occurs as you describe, it will occur only for the platforms on which compilation failed. Then the developer knows to adapt their code to platform differences, by #if statements or any other conditional construct, including conditional statements in the .csproj file.

In other words, we should look at nuget libraries and apps as being umbrella concepts, which is exactly what they are. No new mechanism is needed for multi-targetting, IMHO. In fact this ought to have always included other platforms as well, so that a single csproj could be used for a library or app that targets desktop, Xamarin.iOS and Xamarin.Android. That's what MAUI is moving towards, and what the rust world already has in cargo.

I really think that we can do this today. The underlying tooling has the necessary smarts, but the top-level tooling - the Xamarin msbuild scripts and Visual Studio - prohibit this because they don't duck-type what it means to be a library or an app. We're just not using all the expressive power that the underlying msbuild platform and nuget allow.

marek-safar commented 3 years ago

NSTextAlignment the value depends on the platform it is run ..... it seems that the only way to safely future proof is really having a different TFI,

I don't think the change in enum value requires new TFI, it's an implementation detail that can be covered by different RID for catalyst platform and there will be one.

In fact, we may want to think about this scenario for all the Apple platforms. The current plan is having different TFMs for WatchOS vs iOS vs tvOS. But maybe having a joint TFM with annotations for most people, with the ability of #if with RID for advanced scenarios/native code, is a better approach?

I think having a single TFM would give the developers the most value. I understand this would require more work on our side but having a uniform way to target all apple "modern" platforms would be a big benefit to developers, especially libraries authors.

rolfbjarne commented 3 years ago

I understand this would require more work on our side but having a uniform way to target all apple "modern" platforms would be a big benefit to developers, especially libraries authors.

Won't that require library authors to build their libraries (even the purely managed ones) for each RID instead of for each TFM?

coolbluewater commented 3 years ago

@rolfbjarne, as an example, what would the csproj(s) look like for a) a Xamarin.iOS app and b) a Xamarin.iOS nuget library, each of which runs under iOS, iPadOS and Mac Catalyst, under the two approaches (RID vs TFM)?

marek-safar commented 3 years ago

Won't that require library authors to build their libraries (even the purely managed ones) for each RID instead of for each TFM?

No, it's similar as you build .net5 libraries today. You build for .net5 TFM even though we have hundreds of RIDs for os/arch combinations.

rolfbjarne commented 3 years ago

@marek-safar I don't see how building once can work when you have incompatible API between RIDs:

NSTextAlignment the value depends on the platform it is run ..... it seems that the only way to safely future proof is really having a different TFI,

I don't think the change in enum value requires new TFI, it's an implementation detail that can be covered by different RID for catalyst platform and there will be one.

Enum values can't be an implementation detail, because they're read from the reference assembly and baked into the compiled assembly.