しおメモ

雑多な技術系ブログです。ニッチな内容が多いです。

コードからXcodeのプロジェクトルートを取得する

シミュレータでのデバック時やXcode previewsを利用する際に、差分管理の都合上、プロジェクトにリソースを追加せずにデバッグしたいケースがあります。
そのような場合に、リソースのロードにローカルのパスを参照する必要があるため、Xcodeのプロジェクトルートをコードから探る必要が生じます。

ということでDEBUG onlyで、プロジェクトルートを辿る方法を考えてみます。
(今回はSRCROOTを使っていますが、PROJECT_DIRなどでも同様です。)

ビルド時にスクリプトでInfo.plistに書き込む

PlistBuddyを使って、Info.plistに書き込む方法です。
Build PhasesのCopy Bundle Resourcesの後にスクリプトを追加して、.app配下のInfo.plistに項目を追加します。
(プロジェクト内のInfo.plistには項目は追加されません)

if [ "${CONFIGURATION}" = "Debug" ]; then
  /usr/libexec/PlistBuddy -c "Add :RootDir string ${SRCROOT}" "${BUILT_PRODUCTS_DIR}/${INFOPLIST_PATH}"
fi

PlistBuddyで書き込む際に、ENABLE_USER_SCRIPT_SANDBOXINGNOにする必要があります。

Swiftからは、Bundle.main.infoDictionaryで取得できます。

let projectRootPath = Bundle.main.infoDictionary?["RootDir"] as? String

SRCROOT以外にも、gitで管理している場合はそちらを使うことができます。

filePathマクロを使う

#filePathマクロを使うことで、あるファイルのパスからプロジェクトルートを探す方法です。
ファイルの置き場所とプロジェクトルートの相対パスに処理が依存するので、ファイルの移動の際に注意する必要があります。

let fileUrl = URL(filePath: #filePath)
let components = fileUrl.pathComponents.dropLast().dropFirst() // 相対パスに応じて加工
let projectRootPath = components.joined(separator: "/")

developer.apple.com

リソース置き場に上記の内容を含んだファイルを置いておけば、相対パスの考慮が必要なくなります。

ObjC経由でpreprocessor macroを使う

Swiftからは直接preprocessor macroの値を参照することはできないですが、Objective-Cで定数として定義し直して利用する方法もあります。

Build SettingsのApple Clang - Preperocessingから、Preprocessor MacrosでROOTDIR=$(SRCROOT)のように定義します。

Objective-Cの適当なヘッダーに定数を定義して、Bridging-Headerを介してSwiftから扱います。

#import <Foundation/Foundation.h>

#ifdef DEBUG
static NSString *const projectRootPath = @OS_STRINGIFY(ROOTDIR);
#endif

SwiftからはprojectRootPathで参照できます。

VSCodeの3-way mergeをgitのmergetoolとして利用する

1年ほど前にgitのmergetoolvimdiffからvscodeの3-wayに乗り換えて馴染んできたので、設定方法のメモと布教です。

設定方法

メインのエディタでVSCodeを使っていて、git操作もその中で完結している場合は、VSCode自体の設定でgit.mergeEditortrueにするだけで良いです。
一方で、IDE勢でgit操作だけターミナルを使うような場合は、git configから設定する必要があります。

git config --global merge.tool vscode
git config --global mergetool.vscode.cmd 'code --wait --new-window --merge $LOCAL $REMOTE $BASE $MERGED'

.gitconfigではこのようになります。

[merge]
    tool = vscode
[mergetool "vscode"]
    cmd = code --wait --new-window --merge $LOCAL $REMOTE $BASE $MERGED

簡単な解説を付け加えると、--wait(-w)オプションがVSCodeで該当のファイルが閉じられるまでシェル側で待つ、--new-window(-n)オプションが新しくウィンドウを作成するというものです。

git mergetoolでVSCodeの新しいウィンドウが開き、閉じると次のファイルに移ります。

所感

3-way merge editorは不評だったようで、一時期デフォルトだったものの、外されてしまいました。

github.com

操作は]cやら1doやらを打たずに、"Accept xxx"や、"x Conflict Remaining"をポチポチ押すだけなので、普段あまりvimを使わない場合はこちらの方が直感的で楽に感じました。

scior.hatenablog.com

URLRequestをcurlコマンドに変換する

サーバーサイドエンジニアとやり取りする時など、何かとcurlコマンドが欲しい機会があると思います。
Charlesなどの外部ツールを使っても出来ますが、LLDBで扱えると時短になります。

Swift側のextension

HEADリクエストなどを考慮していないため、厳密ではない部分はありますが、実務で使うようなリクエストにはシンプルなコードで対応できます。
必要に応じて、オプションをcomponentsに追加することで調整可能です。

extension URLRequest {
    var curlCommand: String? {
        guard let method = httpMethod,
              let url = url?.absoluteString else {
            return nil
        }

        var components = ["curl", "-X", method, "'\(url)'"]
        for (key, value) in allHTTPHeaderFields ?? [:] {
            components.append("-H")
            components.append("'\(key): \(value)'")
        }
        if let httpBody, let body = String(data: httpBody, encoding: .utf8) {
            components.append("-d")
            components.append("'\(body)'")
        }

        return components.joined(separator: " ")
    }
}
let request = URLRequest(url: .init(string: "https://www.hogehoge.com")!)
print(request.curlCommand!) // curl -X GET 'https://www.hogehoge.com'

LLDB用のPythonスクリプト

プロダクトに入れる必要は全くないので、swiftファイルをプロジェクト外に置いておき、LLDBのスクリプトで読ませるのがおすすめです。
今回はSwift側で整形するので、Python側の処理はswiftファイルを読み込んで、そのままLLDBに食わせるだけです。

import lldb
import lldbcommon as common
import os


@lldb.command("curl")
def print_curl(debugger, exp, result, dict):
    path = os.path.join(os.path.dirname(__file__), 'swift/URLRequest+curlCommand.swift')
    with open(path, 'r') as f:
        common.evaluate(f.read())

    common.evaluate(f'print({exp}.curlCommand!)')

毎回swiftファイルをロードしてしまうので、 気になる方はPython側で制御を追加してください。

common.evaluateの実装の一例は以下の通りです。

def evaluate(exp):
    options = lldb.SBExpressionOptions()
    options.SetLanguage(lldb.eLanguageTypeSwift)
    options.SetTimeoutInMicroSeconds(3 * 1000 * 1000)
    options.SetTrapExceptions(False)

    frame = (
        lldb.debugger.GetSelectedTarget()
        .GetProcess()
        .GetSelectedThread()
        .GetSelectedFrame()
    )

    value = frame.EvaluateExpression(exp, options)
    error = value.GetError()
    if not error.Success() and error.value != 0x1001:
        print(error)
        
    return value