euphonictechnologies’s diary

Haskell超初心者の日記です。OCamlが好きです。

follow us in feedly

Linux上でSwift Package Managerを使ってビルドするときに躓いたこと

けっこう茨の道です

なんかまだ「初心者でも鼻くそほじりながら日曜日にさらっとね」って感じではないです。なにせコンパイラ、Xcode、Swift Package Manager(以下SPM)すべてが日進月歩でバージョンアップしているのでウェブに書いてあることそのまま試すと全く動かなかったりします。

このブログの内容は2016年11月6日の内容です。Swiftはversion 3.0.1 (swift-3.0.1-RELEASE)を使いUbuntu 14.04上でやりました。

f:id:euphonictechnologies:20161106185709p:plain

まずは

github.com

公式ドキュメント。必読です。私はここを読まずにたくさんの時間を無駄にしました。アホですね。

CとSwift混ぜるとき

モジュールを分ける

SPMはSources下のディレクトリ単位でモジュール扱いをします。Sources直下にあるディレクトリ一つがモジュールという単位になります。

CとSwiftのコードを同じモジュールに混在させるとバリデーションに引っかかります。素直に分けましょう。私のオセロライブラリGrapheneの場合

(Project root)
├── Graphene.xcodeproj
... xcodeprojの中身がドバ~っと
├── LICENSE
├── Package.swift
├── README.md
├── Sources
│   ├── Graphene
│   │   ├── Board.swift
│   │   ├── BoardBuilder.swift
│   │   ├── BoardMediator.swift
... ファイルがドバ~っと
│   └── Intrinsics
│       ├── Intrinsics.c
│       └── include
│           ├── Graphene-Bridging-Header.h
│           ├── Intrinsics.h
│           └── Swift_Bridging_Header.h
└── Tests
    ├── GrapheneTests
    │   └── GrapheneTests.swift
    └── LinuxMain.swift

こんな感じになっています。Swiftコードは(Project root)/Sources/Grapheneというパッケージ名と同じモジュールにまとめてあります。Cのコードは(Project root)/Sources/Intrinsicsというモジュールにしました。Cモジュールにはinclude(すべて小文字)が必要です。なので(Project root)/Sources/Intrinsics/includeディレクトリがあります。モジュール名と同じヘッダファイルが第一に使われます。他のbridging headerは多分必要ないのですが、念のために残してあります。消して壊れたら嫌なので。

Package.swiftにtargetを追加してdependencyを明示する

オフィシャルのドキュメントに書いてあるのですが、dependencyを明示するために依存しているモジュールをPackage.swiftのtargetをきちんと書く必要があります。私のSources/GrapheneはSources/Intrinsicsに依存しています。つまり

import PackageDescription

let package = Package(
    name: "Graphene",
    targets: [
        Target(name: "Graphene", dependencies: ["Intrinsics"]),
    ]
)

こんな感じです。これはCのコードからなるIntrinsicsモジュールをSwiftモジュールGrapheneより先にビルドするためです。

@_silgen_nameはとりあえず外す

なんかCのシンボルがリンク時に見つからないとか言うし@silgen_nameアトリビュートがあるファイルにimport Intrinsicsと書くと今度はシンボルが曖昧とか言われるしわけわかんないのでとりあえず@silgen_nameは消してimport Intrinsicsを残すことにしました。つまり

#if os(Linux)
import Intrinsics
#else
@_silgen_name("_bitScanForward")
    func _bitScanForward(_: UInt64) -> UInt
    
@_silgen_name("_bitPop")
    func _bitPop(_: UInt64) -> UInt
#endif

こんな感じです。当然macOSでXcode上でビルドするためには_silgen_nameが必要なので、こんなふうに分岐させてみました。Linux向け特殊処理が必要なときは今のところこうやってさっさと回避するのが吉のようです。

こうすることでLinux上のコンパイラはIntrinsics内のシンボルをリンクしようとするし、Xcode上では@_silgen_nameアトリビュートを使ってビルドしてくれます。ここらへんは多分ちゃんとしたやり方があると思うのですが、今はこれが精一杯(ルパン)。

ちゃんとstdヘッダをインクルードする

uint64_tのためにCのコードに#include <stdint.h>が必要です。Xcodeでビルドするときはどっかでインクルードされてるから必要なかったりして、意外にハマりました。他にもimplicitに依存しているstdライブラリはちゃんと明示する必要があります。明示しておくほうがお行儀も良いですしね。

これらがうまくいくと

~/projects/Graphite$ swift build
warning: minimum recommended clang is version 3.6, otherwise you may encounter linker errors.
Compile Intrinsics Intrinsics.c
Linking Intrinsics
Compile Swift Module 'Graphene' (27 sources)
<module-includes>:1:1: warning: umbrella header for module 'Intrinsics' does not include header 'Graphene-Bridging-Header.h'
#include "/home/ubuntu/projects/Graphite/Packages/Graphene-1.9.0/Sources/Intrinsics/include/Intrinsics.h"
^
<module-includes>:1:1: warning: umbrella header for module 'Intrinsics' does not include header 'Swift_Bridging_Header.h'
#include "/home/ubuntu/projects/Graphite/Packages/Graphene-1.9.0/Sources/Intrinsics/include/Intrinsics.h"
^
/home/ubuntu/projects/Graphite/Packages/Graphene-1.9.0/Sources/Graphene/EdaxProtocol.swift:28:13: warning: variable 'bb' was never mutated; consider changing to 'let' constant
        var bb = SimpleBitBoard()
        ~~~ ^
        let
/home/ubuntu/projects/Graphite/Packages/Graphene-1.9.0/Sources/Graphene/NegaAlphaSearch.swift:81:26: warning: result of call to 'put(_:x:y:guides:)' is unused
                newBoard.put(turn, x: px, y: py, guides: false)
                         ^  ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
/home/ubuntu/projects/Graphite/Packages/Graphene-1.9.0/Sources/Graphene/BoardMediator.swift:22:9: warning: result of call to 'updateGuides' is unused
        updateGuides(.black)
        ^           ~~~~~~~~
/home/ubuntu/projects/Graphite/Packages/Graphene-1.9.0/Sources/Graphene/SimpleProofSolver.swift:281:33: warning: result of call to 'put(_:x:y:guides:returnChanges:)' is unused
            newBR.boardMediator.put(node.whosTurn, x: px, y: py)
                                ^  ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Compile Swift Module 'Graphite' (1 sources)
Linking ./.build/debug/Graphite

と、GraphiteというGrapheneパッケージを利用した実行ファイルがビルドできました(警告出てますね。恥ずかしい)。CモジュールIntrinsicsが先にコンパイルされているのがわかります。

そのほかSwift on Linuxのメモ

Ubuntu 14.04でやる

RHELとかCentOSでもビルドできたという報告がググルと出ますが、どうも更に茨の道のようです。Ubuntu 14.04が一番鉄板です。16も私のところではライブラリ周りでうまくいかなくて諦めました。ヘタレです。

arc4randomはない

知らなかったのですが、arc4randomはFonudationの中ではなくてBSDのライブラリファンクションなんですね。

developer.apple.com

というわけで当然Linuxのglibcにはないので、代替品を用意する必要があります。私はこんな感じにしました。

#if os(Linux)
    import Glibc
    import SwiftShims
#else
    import Darwin
#endif

func cs_arc4random_uniform(upperBound : UInt32 = UINT32_MAX) -> UInt32 {
    #if os(Linux)
        return _swift_stdlib_cxx11_mt19937_uniform(upperBound)
    #else
        return arc4random_uniform(upperBound)
    #endif
}

こういうときもさっさとLinuxだけ別にしちゃいましょう。_swift_stdlib_cxx11_mt19937_uniformはSwiftShimsにあります。CSPRNGの必要あるのかって気はしますが。他のパターンもSwiftShimsで結構カバーできるような気がします。

まとめ

github.com

とりあえずLinux上でビルドができるようになりました。というわけでLinux上で開発ができるようにEmacsの環境を整えようと思います。

Xcodeで書きたいのはやまやまなのですが、SwiftのコンパイラのバージョンがmacOS用のツールチェーンだと微妙に古いのでうまくいかず、かといってオフィシャルのベータをインストールしたりしてぐちゃぐちゃにするとiOS用のビルド環境がなくなっちゃうのでLinux上で頑張ることにします。

2017.02.05追記

開発はXcodeだけでLinuxと両方サポートできています。なのでXcodeで快適開発しちゃいましょう。Emacsが快適じゃないとは言ってないですよ。Emacsも大好きです。