[Draft] Adding a Build Configuration Import Test


(Erica Sadun) #1

Here's another one, hopefully with a little more grounding than the debug test.
I welcome feedback both positive and critical, -- E

Adding an Build Configuration Import Test

Proposal: SE-00XX
Author(s): Erica Sadun <http://github.com/erica>
Status: TBD
Review manager: TBD
<https://gist.github.com/erica/b7f4226b8201945602f2#introduction>Introduction

Expanding the build configuration suite to test for the ability to import certain modules was first introduced <http://article.gmane.org/gmane.comp.lang.swift.evolution/7516/match=darwin> on the Swift-Evolution list by Kevin Ballard. Although his initial idea (checking for Darwin to differentiate Apple targets from non-Apple targets) proved problematic, developers warmly greeted the notion of an import-based configuration test. Dmitri Gribenko wrote, "There's a direction that we want to move to a unified name for the libc module for all platform, so 'can import Darwin' might not be a viable long-term strategy." Testing for imports offers advantages that stand apart from this one use-case: to test for API availability before use.

<https://gist.github.com/erica/b7f4226b8201945602f2#motivation>Motivation

Swift's existing set of build configurations specify platform differences, not module commonalities. For example, UIKit enables you to write view code supported on both iOS and tvOS. SpriteKit allows common code to render on OS X, iOS, and tvOS that would require an alternate UI on Linux. Testing for Metal support or Media Player would guard code that will not function on the simulator. If the simulator adopted these modules at some future time, the code would naturally expand to provide compatible execution without source modification.

#if canImport(UIKit)
   // UIKit-based code
   #elseif canImport(Cocoa)
   // OSX code
   #elseif
   // Workaround/text, whatever
#endif
Guarding code with operating system tests can be less future-proofed than testing for module support. Excluding OS X to use UIColor creates code that might eventually find its way to a Linux plaform. Targeting Apple platforms by inverting a test for Linux essentially broke after the introduction of Windows and FreeBSD build configurations:

// Exclusive os tests are brittle
#if !os(Linux)
   // Matches OSX, iOS, watchOS, tvOS, Windows, FreeBSD
#endif
Inclusive OS tests (if os1 || os2 || os3...) must be audited each time the set of possible platforms expands. In addition, compound build statements are harder to write, to validate, and are more confusing to read. They are more prone to errors than a single test that's tied to the API capabilities used by the code it guards.

Evan Maloney writes, "Being able to test for the importability of a given module/framework at runtime would be extremely helpful. We use several frameworks that are only available in a subset of the platforms we support, and on only certain OS versions. To work around this problem now, we dynamically load frameworks from Obj-C only when we're running on an OS version we know is supported by the framework(s) in question. We can't dynamically load them from Swift because if they're included in an import, the runtime tries to load it right away, leading to a crash on any unsupported platform. The only way to selectively load dynamic frameworks at runtime is to do it via Obj-C. Some sort of check like the ones you propose should let us avoid this."

<https://gist.github.com/erica/b7f4226b8201945602f2#detail-design>Detail Design

#if canImport(module-name) tests for module support by name. My proposed name uses lower camelCase, which is not currently used in the current build configuration vocabulary but is (in my opinion) clearer in intention than the other two terms brought up on the evolution list, #if imports() and #if supports().

This build configuration does not import the module it names
This build configuration is intended to differentiate API access
This build configuration should not be used to differentiate platforms
The supplied module token is an arbitrary string. It does not belong to an enumerated set of known members as this configuration test is intended for use with both first and third party modules for the greatest flexibility. At compile time, Swift determines whether the module can or cannot be linked and builds accordingly.
#if canImport(module)
    import module
    // use module APIs safely
#endif

#if canImport(module)
    // provide solution with module APIs
    #else
    // provide alternative solution that does not depend on that module
#endif
<https://gist.github.com/erica/b7f4226b8201945602f2#current-art>Current Art

Swift currently supports the following configuration tests:

The literals true and false
The os() function that tests for OSX, iOS, watchOS, tvOS, Linux, Windows, and FreeBSD
The arch() function that tests for x86_64, arm, arm64, i386, powerpc64, and powerpc64le
The swift() function that tests for specific Swift language releases, e.g. swift(>=2.2)
<https://gist.github.com/erica/b7f4226b8201945602f2#alternatives-considered>Alternatives Considered

There are no alternatives considered.


(Brent Royal-Gordon) #2

#if canImport(UIKit)
   // UIKit-based code
   #elseif canImport(Cocoa)
   // OSX code
   #elseif
   // Workaround/text, whatever
#endif

My question is simple: Are there cases where you want to test if you can import something, but you don't want to *actually* import it? If not, should we perhaps just do this?

  #if import UIKit
    // UIKit-based code, has UIKit available
  #elseif import Cocoa
    // OS X code, has Cocoa available
  #else
    // Fallback
  #endif

···

--
Brent Royal-Gordon
Architechies


(Haravikk) #3

Strong +1, in my experience in other languages OS based tests (or worse, browser tests in Javascript, *shudders*) are far too often misused, modules are definitely the better way to go.

My only question is whether there should be a way to test minimum module versions? If I add support for a module but target a particularly recent version then that support won’t compile for platforms with older versions of the same module, so I should be able to prevent them from being a match.

···

On 22 Mar 2016, at 22:14, Erica Sadun via swift-evolution <swift-evolution@swift.org> wrote:

Here's another one, hopefully with a little more grounding than the debug test.
I welcome feedback both positive and critical, -- E

Adding an Build Configuration Import Test

Proposal: SE-00XX
Author(s): Erica Sadun <http://github.com/erica>
Status: TBD
Review manager: TBD
<https://gist.github.com/erica/b7f4226b8201945602f2#introduction>Introduction

Expanding the build configuration suite to test for the ability to import certain modules was first introduced <http://article.gmane.org/gmane.comp.lang.swift.evolution/7516/match=darwin> on the Swift-Evolution list by Kevin Ballard. Although his initial idea (checking for Darwin to differentiate Apple targets from non-Apple targets) proved problematic, developers warmly greeted the notion of an import-based configuration test. Dmitri Gribenko wrote, "There's a direction that we want to move to a unified name for the libc module for all platform, so 'can import Darwin' might not be a viable long-term strategy." Testing for imports offers advantages that stand apart from this one use-case: to test for API availability before use.

<https://gist.github.com/erica/b7f4226b8201945602f2#motivation>Motivation

Swift's existing set of build configurations specify platform differences, not module commonalities. For example, UIKit enables you to write view code supported on both iOS and tvOS. SpriteKit allows common code to render on OS X, iOS, and tvOS that would require an alternate UI on Linux. Testing for Metal support or Media Player would guard code that will not function on the simulator. If the simulator adopted these modules at some future time, the code would naturally expand to provide compatible execution without source modification.

#if canImport(UIKit)
   // UIKit-based code
   #elseif canImport(Cocoa)
   // OSX code
   #elseif
   // Workaround/text, whatever
#endif
Guarding code with operating system tests can be less future-proofed than testing for module support. Excluding OS X to use UIColor creates code that might eventually find its way to a Linux plaform. Targeting Apple platforms by inverting a test for Linux essentially broke after the introduction of Windows and FreeBSD build configurations:

// Exclusive os tests are brittle
#if !os(Linux)
   // Matches OSX, iOS, watchOS, tvOS, Windows, FreeBSD
#endif
Inclusive OS tests (if os1 || os2 || os3...) must be audited each time the set of possible platforms expands. In addition, compound build statements are harder to write, to validate, and are more confusing to read. They are more prone to errors than a single test that's tied to the API capabilities used by the code it guards.

Evan Maloney writes, "Being able to test for the importability of a given module/framework at runtime would be extremely helpful. We use several frameworks that are only available in a subset of the platforms we support, and on only certain OS versions. To work around this problem now, we dynamically load frameworks from Obj-C only when we're running on an OS version we know is supported by the framework(s) in question. We can't dynamically load them from Swift because if they're included in an import, the runtime tries to load it right away, leading to a crash on any unsupported platform. The only way to selectively load dynamic frameworks at runtime is to do it via Obj-C. Some sort of check like the ones you propose should let us avoid this."

<https://gist.github.com/erica/b7f4226b8201945602f2#detail-design>Detail Design

#if canImport(module-name) tests for module support by name. My proposed name uses lower camelCase, which is not currently used in the current build configuration vocabulary but is (in my opinion) clearer in intention than the other two terms brought up on the evolution list, #if imports() and #if supports().

This build configuration does not import the module it names
This build configuration is intended to differentiate API access
This build configuration should not be used to differentiate platforms
The supplied module token is an arbitrary string. It does not belong to an enumerated set of known members as this configuration test is intended for use with both first and third party modules for the greatest flexibility. At compile time, Swift determines whether the module can or cannot be linked and builds accordingly.
#if canImport(module)
    import module
    // use module APIs safely
#endif

#if canImport(module)
    // provide solution with module APIs
    #else
    // provide alternative solution that does not depend on that module
#endif
<https://gist.github.com/erica/b7f4226b8201945602f2#current-art>Current Art

Swift currently supports the following configuration tests:

The literals true and false
The os() function that tests for OSX, iOS, watchOS, tvOS, Linux, Windows, and FreeBSD
The arch() function that tests for x86_64, arm, arm64, i386, powerpc64, and powerpc64le
The swift() function that tests for specific Swift language releases, e.g. swift(>=2.2)
<https://gist.github.com/erica/b7f4226b8201945602f2#alternatives-considered>Alternatives Considered

There are no alternatives considered.
_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution


(Chris Lattner) #4

Very interesting proposal. Note that this is directly analogous to the Clang extension “__has_include”. __has_include has been useful, and the C++ committee is discussing standardizing the functionality there.

http://clang.llvm.org/docs/LanguageExtensions.html#include-file-checking-macros

-Chris

···

On Mar 22, 2016, at 3:14 PM, Erica Sadun via swift-evolution <swift-evolution@swift.org> wrote:

<https://gist.github.com/erica/b7f4226b8201945602f2#introduction>Introduction

Expanding the build configuration suite to test for the ability to import certain modules was first introduced <http://article.gmane.org/gmane.comp.lang.swift.evolution/7516/match=darwin> on the Swift-Evolution list by Kevin Ballard. Although his initial idea (checking for Darwin to differentiate Apple targets from non-Apple targets) proved problematic, developers warmly greeted the notion of an import-based configuration test. Dmitri Gribenko wrote, "There's a direction that we want to move to a unified name for the libc module for all platform, so 'can import Darwin' might not be a viable long-term strategy." Testing for imports offers advantages that stand apart from this one use-case: to test for API availability before use.


#5

Not sure if its because I mainly live in a world of multi-architectures C cross-compiling, but for example if you canInclude some powerpc header, it does’t mean that your are compiling for powerpc. So, IMO, I think that #if didImport(Cocoa) would be a better wording. This effectively prevent one from doing:

#if didImport(Cocoa)
  import Cocoa // Pointless, won’t work
#endif

So for conditionally import, we would still be forced to use some form of #if os(OSX)

Dany

···

Le 22 mars 2016 à 18:14, Erica Sadun via swift-evolution <swift-evolution@swift.org> a écrit :

<https://gist.github.com/erica/b7f4226b8201945602f2#motivation>Motivation

Swift's existing set of build configurations specify platform differences, not module commonalities. For example, UIKit enables you to write view code supported on both iOS and tvOS. SpriteKit allows common code to render on OS X, iOS, and tvOS that would require an alternate UI on Linux. Testing for Metal support or Media Player would guard code that will not function on the simulator. If the simulator adopted these modules at some future time, the code would naturally expand to provide compatible execution without source modification.

#if canImport(UIKit)
   // UIKit-based code
   #elseif canImport(Cocoa)
   // OSX code
   #elseif
   // Workaround/text, whatever
#endif


(Andrey Tarantsov) #6

Strong +1, very useful.

Perhaps importable instead of canImport? Looks and reads better:

// UXKit at WWDC'16, pretty please!
#if importable(UIKit)
typealias UXColor = UIColor
#elseif importable(Cocoa)
typealias UXColor = NSColor
#else
// meh
#endif

A.


(Erica Sadun) #7

I'm preparing a separate platform proposal. I want to make clear that the intent is to use the
framework being tested. However, the test that does the import may be separated from the code
that uses the framework, which is why the call does not import.

-- E

···

On Mar 22, 2016, at 10:24 PM, Brent Royal-Gordon <brent@architechies.com> wrote:

#if canImport(UIKit)
  // UIKit-based code
  #elseif canImport(Cocoa)
  // OSX code
  #elseif
  // Workaround/text, whatever
#endif

My question is simple: Are there cases where you want to test if you can import something, but you don't want to *actually* import it? If not, should we perhaps just do this?

  #if import UIKit
    // UIKit-based code, has UIKit available
  #elseif import Cocoa
    // OS X code, has Cocoa available
  #else
    // Fallback
  #endif

--
Brent Royal-Gordon
Architechies


(Jeff Kelley) #8

This might be crazy, but we could also reuse try here:

import Foundation
try import UIKit {
  // UIKit Code
}
else try import Cocoa
  // Cocoa Code
}

This would attempt to import UIKit and if it succeeded, run the code in the closure.

Jeff Kelley

SlaunchaMan@gmail.com | @SlaunchaMan <https://twitter.com/SlaunchaMan> | jeffkelley.org <http://jeffkelley.org/>

···

On Mar 23, 2016, at 9:27 PM, Dany St-Amant via swift-evolution <swift-evolution@swift.org> wrote:

Le 22 mars 2016 à 18:14, Erica Sadun via swift-evolution <swift-evolution@swift.org <mailto:swift-evolution@swift.org>> a écrit :

<https://gist.github.com/erica/b7f4226b8201945602f2#motivation>Motivation

Swift's existing set of build configurations specify platform differences, not module commonalities. For example, UIKit enables you to write view code supported on both iOS and tvOS. SpriteKit allows common code to render on OS X, iOS, and tvOS that would require an alternate UI on Linux. Testing for Metal support or Media Player would guard code that will not function on the simulator. If the simulator adopted these modules at some future time, the code would naturally expand to provide compatible execution without source modification.

#if canImport(UIKit)
   // UIKit-based code
   #elseif canImport(Cocoa)
   // OSX code
   #elseif
   // Workaround/text, whatever
#endif

Not sure if its because I mainly live in a world of multi-architectures C cross-compiling, but for example if you canInclude some powerpc header, it does’t mean that your are compiling for powerpc. So, IMO, I think that #if didImport(Cocoa) would be a better wording. This effectively prevent one from doing:

#if didImport(Cocoa)
  import Cocoa // Pointless, won’t work
#endif

So for conditionally import, we would still be forced to use some form of #if os(OSX)

Dany

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution


(Pierre Monod-Broca) #9

My guess is most often one will want to import exactly what one is testing (Brent example) but I guess it would be very handy to do something like

#if canImport(Foo)
    import Foo.Bar as FooBar
#endif

In this case both form would be relevant.

Also +1 on Haraviks point:

My only question is whether there should be a way to test minimum module versions? If I add support for a module but target a particularly recent version then that support won’t compile for platforms with older versions of the same module, so I should be able to prevent them from being a match.

Pierre

···

Le 23 mars 2016 à 05:28, Erica Sadun via swift-evolution <swift-evolution@swift.org> a écrit :

I'm preparing a separate platform proposal. I want to make clear that the intent is to use the
framework being tested. However, the test that does the import may be separated from the code
that uses the framework, which is why the call does not import.

-- E

On Mar 22, 2016, at 10:24 PM, Brent Royal-Gordon <brent@architechies.com> wrote:

#if canImport(UIKit)
// UIKit-based code
#elseif canImport(Cocoa)
// OSX code
#elseif
// Workaround/text, whatever
#endif

My question is simple: Are there cases where you want to test if you can import something, but you don't want to *actually* import it? If not, should we perhaps just do this?

   #if import UIKit
       // UIKit-based code, has UIKit available
   #elseif import Cocoa
       // OS X code, has Cocoa available
   #else
       // Fallback
   #endif

--
Brent Royal-Gordon
Architechies

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution


(Erica Sadun) #10

The problem with your approach is that the Cocoa code would have to compile under iOS and
I'm afraid that's a no-go. Build-configuration tests enable entire blocks of code to be removed
from compilation.

-- E

···

On Mar 28, 2016, at 10:37 PM, Jeff Kelley via swift-evolution <swift-evolution@swift.org> wrote:

This might be crazy, but we could also reuse try here:

import Foundation
try import UIKit {
  // UIKit Code
}
else try import Cocoa
  // Cocoa Code
}

This would attempt to import UIKit and if it succeeded, run the code in the closure.


(Jeff Kelley) #11

Why would it have to compile under iOS? The “import Cocoa” bit is behind an else clause.

Jeff Kelley

SlaunchaMan@gmail.com | @SlaunchaMan <https://twitter.com/SlaunchaMan> | jeffkelley.org <http://jeffkelley.org/>

···

On Mar 29, 2016, at 12:44 AM, Erica Sadun <erica@ericasadun.com> wrote:

On Mar 28, 2016, at 10:37 PM, Jeff Kelley via swift-evolution <swift-evolution@swift.org> wrote:

This might be crazy, but we could also reuse try here:

import Foundation
try import UIKit {
  // UIKit Code
}
else try import Cocoa
  // Cocoa Code
}

This would attempt to import UIKit and if it succeeded, run the code in the closure.

The problem with your approach is that the Cocoa code would have to compile under iOS and
I'm afraid that's a no-go. Build-configuration tests enable entire blocks of code to be removed
from compilation.

-- E


(Brent Royal-Gordon) #12

My guess is most often one will want to import exactly what one is testing (Brent example) but I guess it would be very handy to do something like

#if canImport(Foo)
   import Foo.Bar as FooBar
#endif

I don't see any particular reason you couldn't support anything `import` can support in an `#if import`.

Also +1 on Haraviks point:

My only question is whether there should be a way to test minimum module versions? If I add support for a module but target a particularly recent version then that support won’t compile for platforms with older versions of the same module, so I should be able to prevent them from being a match.

I think we'd be better off extending our existing `@available` and `#availability` mechanisms to test module versions in addition to platform versions. Two small, orthogonal features are better than one big, confused one.

···

--
Brent Royal-Gordon
Architechies


(Erica Sadun) #13

`import as ` does not fall under the scope of this proposal. I have been already discussing it
as part of an existing issue about package name conflicts on -build-dev, with the understanding
that this portion would have to be raised in -evolution. It's on my list for separate pitching.
An early draft is here: https://gist.github.com/erica/c6553a5f6f35e7462074, and the most
likely pathway would something like:

1. No change in the originating package description.

import PackageDescription

let package = Package(
    name: "SwiftString",
)

2. No change in the consuming package description.

import PackageDescription
let package = Package (
    name: "myutility",
    dependencies: [
       .Package(url: "https://github.com/erica/SwiftString.git",
          majorVersion: 1),
       .Package(url: "https://github.com/nudas/SwiftString.git",
          majorVersion: 1),
       .Package(url: "http://github.com/erica/SomeStringOtherPackage.git",
                majorVersion: 1), // just throwing some package in there
    ],
)

3. When unpacking, SwiftPM detects name overlap, automatically uses reverse domain name for unpacking

In the current Swift PM:
    fatal: destination path '/home/erica/Work/test/Packages/SwiftString' already exists and is not an empty directory.

Instead:
* Would unpack into com.github.erica.SwiftString and com.github.nudas.SwiftString, automatically reversing the url
* From Swift files, since "import: SwiftString" allow import as erica.SwiftString, github.erica.SwiftString, or com.github.erica.SwiftString

4. Propose separately, "import as" as a language enhancement.

-- E

···

On Mar 23, 2016, at 12:59 AM, Pierre Monod-Broca <pierremonodbroca@gmail.com> wrote:

My guess is most often one will want to import exactly what one is testing (Brent example) but I guess it would be very handy to do something like

#if canImport(Foo)
   import Foo.Bar as FooBar
#endif

In this case both form would be relevant.


(Pierre Monod-Broca) #14

Good points.

Should we allow the following then? And then should Bar be imported if Baz is not present?

#if import Foo.Bar && import Foo.Baz
#endif

I also want to mention other forms for further discussion:

#if import Foo.Bar as FooBar
#endif

#if !import Foo
#endif

Pierre

···

Le 23 mars 2016 à 08:21, Brent Royal-Gordon <brent@architechies.com> a écrit :

My guess is most often one will want to import exactly what one is testing (Brent example) but I guess it would be very handy to do something like

#if canImport(Foo)
  import Foo.Bar as FooBar
#endif

I don't see any particular reason you couldn't support anything `import` can support in an `#if import`.

Also +1 on Haraviks point:

My only question is whether there should be a way to test minimum module versions? If I add support for a module but target a particularly recent version then that support won’t compile for platforms with older versions of the same module, so I should be able to prevent them from being a match.

I think we'd be better off extending our existing `@available` and `#availability` mechanisms to test module versions in addition to platform versions. Two small, orthogonal features are better than one big, confused one.

--
Brent Royal-Gordon
Architechies


(Erica Sadun) #15

`try` is a run-time call.

-- E

···

On Mar 28, 2016, at 10:51 PM, Jeff Kelley <slaunchaman@gmail.com> wrote:

Why would it have to compile under iOS? The “import Cocoa” bit is behind an else clause.

Jeff Kelley

SlaunchaMan@gmail.com <mailto:SlaunchaMan@gmail.com> | @SlaunchaMan <https://twitter.com/SlaunchaMan> | jeffkelley.org <http://jeffkelley.org/>

On Mar 29, 2016, at 12:44 AM, Erica Sadun <erica@ericasadun.com <mailto:erica@ericasadun.com>> wrote:

On Mar 28, 2016, at 10:37 PM, Jeff Kelley via swift-evolution <swift-evolution@swift.org <mailto:swift-evolution@swift.org>> wrote:

This might be crazy, but we could also reuse try here:

import Foundation
try import UIKit {
  // UIKit Code
}
else try import Cocoa
  // Cocoa Code
}

This would attempt to import UIKit and if it succeeded, run the code in the closure.

The problem with your approach is that the Cocoa code would have to compile under iOS and
I'm afraid that's a no-go. Build-configuration tests enable entire blocks of code to be removed
from compilation.

-- E


(Erica Sadun) #16

Pull Request 229: https://github.com/apple/swift-evolution/pull/229

-- E