How to conditionally polyfill String.init(unsafeUninitializedCapacity:_:) by macOS version?

i’m getting this CI error:

error: 'init(unsafeUninitializedCapacity:initializingUTF8With:)' 
is only available in macOS 11.0 or newer
        try .init(unsafeUninitializedCapacity: 2 * MemoryLayout<Words>.size)
             ^
note: add 'if #available' version check
        try .init(unsafeUninitializedCapacity: 2 * MemoryLayout<Words>.size)
             ^
note: add @available attribute to enclosing static method
    func encodeBigEndian<Words>(_ words:Words, as _:String.Type = String.self, 
         ^
note: add @available attribute to enclosing enum
enum Base16 
     ^
error: fatalError

so i tried to fix it with

@available(macOS, introduced: 11.0)
@inlinable public static 
func encodeBigEndian<Words>(_ words:Words, as _:String.Type = String.self, 
    by ascii:(UInt8) throws -> UInt8) rethrows -> String
{
}
@available(macOS, obsoleted: 11.0)
@inlinable public static 
func encodeBigEndian<Words>(_ words:Words, as _:String.Type = String.self, 
    by ascii:(UInt8) throws -> UInt8) rethrows -> String
{
}

but that didn’t work:

Building for debugging...
error: invalid redeclaration of 'encodeBigEndian(_:as:by:)'
    func encodeBigEndian<Words>(_ words:Words, as _:String.Type = String.self, 
         ^

the only thing that worked was the (quite gnarly):

@inlinable public static 
func encodeBigEndian<Words>(_ words:Words, as _:String.Type = String.self, 
    by ascii:(UInt8) throws -> UInt8) rethrows -> String
{
    #if os(macOS)
    if #available(macOS 11.0, *)
    {
        return .init(decoding: try Self.encodeBigEndian(words, as: [UInt8].self, by: ascii), 
            as: Unicode.UTF8.self)
    }
    else 
    {
        return try .init(unsafeUninitializedCapacity: 2 * MemoryLayout<Words>.size)
        {
            var utf8:UnsafeMutableBufferPointer<UInt8> = $0
            try Self.encodeBigEndian(words, utf8: &utf8, by: ascii)
            return $0.count
        }
    }
    #else 
    try .init(unsafeUninitializedCapacity: 2 * MemoryLayout<Words>.size)
    {
        var utf8:UnsafeMutableBufferPointer<UInt8> = $0
        try Self.encodeBigEndian(words, utf8: &utf8, by: ascii)
        return $0.count
    }
    #endif 
}

is there a better way to deal with this?

1 Like

No need for the #if mac(OS) there, you can check all Apple OSes at once and then fallback. The initial availability error only included your currently building platform.

the #if os(macOS) is there to preclude the runtime availability check on linux, as the library is primarily meant for server users.

In that case I would just check #if os(Linux) || os(Windows), along with a Swift version check if you really want. If you don't care about Apple availability, just set your deployment target high enough to avoid the issue.

i’m trying to degrade gracefully on platforms that aren’t an Amazon Linux instance running swift 5.6. prior to adding this API, the library (swift-hash) had a full compatibility matrix on the swift package index.

since the project’s CI can only do macOS builds (no iOS, tvOS, or watchOS), i don’t really have a way to test the other platforms until the swift package index tries and fails to build the package later today. which feels like a pretty terrible workflow.

i really wish it was easier to write packages that are compatible with more platforms, at this point i really don’t know if it’s worth the effort to get swift-hash working on iOS, tvOS, watchOS, etc.

If you have a CI builder for macOS you should be able to build for all of Apple's other OSes unless you don't have Xcode at all.

Otherwise I'm not sure what you want here. If you want more macOS / Apple OS support you'll need availability checks like this since the versions are runtime limited. And if you want to avoid the availability check even on Linux, then you'll also have to check OS target from the compiler. You could test and see if the availability check actually has any impact on Linux but since you're targeting server development, limiting your Apple deployment targets to eliminate availability checking seems like a fine idea.

example? :slight_smile: (here is my current GitHub action)

personally it doesn’t matter to me if the package doesn’t build for an Apple Watch, this is just me trying to do my part for the community.

this ended up working for the entire CI matrix:

@inlinable public static 
func encodeBigEndian<Words>(_ words:Words, as _:String.Type = String.self, 
    by ascii:(UInt8) throws -> UInt8) rethrows -> String
{
    #if os(macOS) || os(iOS) || os(tvOS) || os(watchOS) 
    if #available(macOS 11.0, iOS 14.0, tvOS 14.0, watchOS 14.0, *)
    {
        return try .init(unsafeUninitializedCapacity: 2 * MemoryLayout<Words>.size)
        {
            var utf8:UnsafeMutableBufferPointer<UInt8> = $0
            try Self.encodeBigEndian(words, utf8: &utf8, by: ascii)
            return $0.count
        }
    }
    else 
    {
        return .init(decoding: try Self.encodeBigEndian(words, as: [UInt8].self, by: ascii), 
            as: Unicode.UTF8.self)
    }
    #elseif swift(>=5.4)
    try .init(unsafeUninitializedCapacity: 2 * MemoryLayout<Words>.size)
    {
        var utf8:UnsafeMutableBufferPointer<UInt8> = $0
        try Self.encodeBigEndian(words, utf8: &utf8, by: ascii)
        return $0.count
    }
    #else 
    return .init(decoding: try Self.encodeBigEndian(words, as: [UInt8].self, by: ascii), 
        as: Unicode.UTF8.self)
    #endif 
}

You can find plenty of examples online, including SPI's build commands, but the short of it is that you use xcodebuild to find the scheme it generates for your package, then build that scheme for the relevant platform.

Given Apple's limited support for Swift on their own platforms, most projects limit support to whatever version of Xcode is required to ship to the store (Xcode 13 ATM, so Swift 5.5), with OS support dependent on the features you need. For your usage there doesn't seem to be much value in supporting Swift <5.5, but that's up to you. I think your desire to avoid the availability check at runtime is really what's expanding the cases you handle, but that's up to you.

1 Like

i got as far as

xcodebuild build -scheme swift-hash-Package -destination generic/platform=ios

but i keep getting

error: unable to resolve product type 'com.apple.product-type.tool' for platform 'iphoneos' (in target 'sha2-tests' from project 'swift-hash')

i thought this was because of executables in the Package.swift, so i tried conditionally excluding them, but i still get the same error.

#if os(iOS) || os(tvOS) || os(watchOS) 
let tools:[Product] = []
let executables:[Target] = []
#else 
let tools:[Product] = 
[
    .executable(name: "sha2-tests", targets: ["SHA2Tests"]),
]
let executables:[Target] = 
[
    .executableTarget(name: "SHA2Tests", 
        dependencies: 
        [
            .target(name: "SHA2"),
        ],
        path: "Tests/SHA2"),
]
#endif 

any ideas?

To the original question, you can use our polyfill.

1 Like

As noted in your side question (Why won’t swift-hash build on the Swift Package Index?), this is likely because you haven't correctly excluded your executable target.