diff --git a/.githooks/post-checkout b/.githooks/post-checkout new file mode 100755 index 000000000..96e7f3a78 --- /dev/null +++ b/.githooks/post-checkout @@ -0,0 +1,8 @@ +#!/bin/sh +command -v git-lfs >/dev/null 2>&1 || { echo >&2 "\nThis repository is configured for Git LFS but 'git-lfs' was not found on your path. If you no longer wish to use Git LFS, remove this hook by deleting '.git/hooks/post-checkout'.\n"; exit 2; } +git lfs post-checkout "$@" + +#!/bin/bash +export PATH="$PATH:/opt/homebrew/bin" + +xcodegen \ No newline at end of file diff --git a/.githooks/post-commit b/.githooks/post-commit new file mode 100755 index 000000000..e5230c305 --- /dev/null +++ b/.githooks/post-commit @@ -0,0 +1,3 @@ +#!/bin/sh +command -v git-lfs >/dev/null 2>&1 || { echo >&2 "\nThis repository is configured for Git LFS but 'git-lfs' was not found on your path. If you no longer wish to use Git LFS, remove this hook by deleting '.git/hooks/post-commit'.\n"; exit 2; } +git lfs post-commit "$@" diff --git a/.githooks/post-merge b/.githooks/post-merge new file mode 100755 index 000000000..c99b752a5 --- /dev/null +++ b/.githooks/post-merge @@ -0,0 +1,3 @@ +#!/bin/sh +command -v git-lfs >/dev/null 2>&1 || { echo >&2 "\nThis repository is configured for Git LFS but 'git-lfs' was not found on your path. If you no longer wish to use Git LFS, remove this hook by deleting '.git/hooks/post-merge'.\n"; exit 2; } +git lfs post-merge "$@" diff --git a/.githooks/pre-commit b/.githooks/pre-commit new file mode 100755 index 000000000..bf5372998 --- /dev/null +++ b/.githooks/pre-commit @@ -0,0 +1,10 @@ +#!/bin/bash + +export PATH="$PATH:/opt/homebrew/bin" + +if ! swiftformat --lint . > /dev/null 2>&1 +then + echo "pre-commit: Commit aborted due to SwiftFormat warnings. Please check the automatically generated fixes and try again" + swiftformat . > /dev/null 2>&1 + exit 1 +fi \ No newline at end of file diff --git a/.githooks/pre-push b/.githooks/pre-push new file mode 100755 index 000000000..216e91527 --- /dev/null +++ b/.githooks/pre-push @@ -0,0 +1,3 @@ +#!/bin/sh +command -v git-lfs >/dev/null 2>&1 || { echo >&2 "\nThis repository is configured for Git LFS but 'git-lfs' was not found on your path. If you no longer wish to use Git LFS, remove this hook by deleting '.git/hooks/pre-push'.\n"; exit 2; } +git lfs pre-push "$@" diff --git a/.gitignore b/.gitignore index 3e4e095b7..e922820c2 100644 --- a/.gitignore +++ b/.gitignore @@ -35,6 +35,9 @@ build Tools/Scripts/element-android /vendor/ +## brew +Brewfile.lock.json + ## macOS Files .DS_Store ._* diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 7240d4785..56d798a65 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -6,11 +6,24 @@ Element iOS support can be found in this room: [![Element iOS Matrix room #eleme ## Setting up a development environment +### Setup Project + +It's mandatory to have [homebrew](https://brew.sh/) installed on your mac, and run after the checkout: + +``` +swift run tools setup-project +``` + +This will: +- Install various brew dependencies required for the project (like xcodegen). +- Set up git to use the shared githooks from the repo, instead of the default ones. +- Automatically run xcodegen for the first time. + ### Xcode We suggest using an Xcode version later than 13.2.1. -The Xcode project can be directly compiled after checkout through the shared ElementX scheme which includes the main application as well as the unit and UI tests. +The Xcode project can be directly compiled through the shared ElementX scheme which includes the main application as well as the unit and UI tests. The Xcode project itself is generated through [xcodegen](https://github.com/yonaskolb/XcodeGen) so any changes shouldn't be made directly to it but to the configuration files. @@ -38,12 +51,20 @@ The project depends on some tools for the build process. These are all included brew bundle ``` -Git LFS is used to store UI test snapshots. After cloning the repo this can be configured by running +Git LFS is used to store UI test snapshots. `swift run tools setup-project` will already install it, however it can also be installed after a checkout by running: ``` git lfs install ``` +### Githooks + +The project uses its own shared githooks stored in the .githooks folder, you will need to configure git to use such folder, this is already done if you have run the setup tool with `swift run tools setup-project` otherwise you would need to run: + +``` +git config core.hooksPath .githooks +``` + ### Continuous Integration ElementX uses Fastlane for running actions on the CI and tries to keep the configuration confined to either [fastlane](fastlane/Fastfile) or [xcodegen](project.yml). diff --git a/Tools/Sources/BuildSDK.swift b/Tools/Sources/BuildSDK.swift index 8600050b3..b7e09165b 100644 --- a/Tools/Sources/BuildSDK.swift +++ b/Tools/Sources/BuildSDK.swift @@ -11,12 +11,10 @@ struct BuildSDK: ParsableCommand { @Option(help: "The target to build for such as aarch64-apple-ios. Omit this option to build for all targets.") var target: String? - private var projectDirectoryURL: URL { URL(filePath: FileManager.default.currentDirectoryPath) } - private var parentDirectoryURL: URL { projectDirectoryURL.deletingLastPathComponent() } + private var parentDirectoryURL: URL { Utilities.projectDirectoryURL.deletingLastPathComponent() } private var sdkDirectoryURL: URL { parentDirectoryURL.appending(path: "matrix-rust-sdk") } enum Error: LocalizedError { - case scriptFailed case rustupOutputFailure case missingRustTargets([String]) case failureParsingProjectYAML @@ -49,7 +47,7 @@ struct BuildSDK: ParsableCommand { /// but only when the ``target`` option hasn't been supplied. func checkRustupTargets() throws { guard target == nil else { return } - guard let output = try zsh("rustup show", workingDirectoryURL: projectDirectoryURL) else { throw Error.rustupOutputFailure } + guard let output = try Utilities.zsh("rustup show") else { throw Error.rustupOutputFailure } var requiredTargets = [ "aarch64-apple-darwin": false, @@ -71,13 +69,13 @@ struct BuildSDK: ParsableCommand { /// Clones the Rust SDK if a copy isn't found in the parent directory. func cloneSDKIfNeeded() throws { guard !FileManager.default.fileExists(atPath: sdkDirectoryURL.path) else { return } - try zsh("git clone https://github.com/matrix-org/matrix-rust-sdk", workingDirectoryURL: parentDirectoryURL) + try Utilities.zsh("git clone https://github.com/matrix-org/matrix-rust-sdk", workingDirectoryURL: parentDirectoryURL) } /// Checkout the specified branch of the SDK if supplied. func checkoutBranchIfSupplied() throws { guard let branch else { return } - try zsh("git checkout \(branch)", workingDirectoryURL: sdkDirectoryURL) + try Utilities.zsh("git checkout \(branch)", workingDirectoryURL: sdkDirectoryURL) } /// Build the Rust SDK as an XCFramework with the debug profile. @@ -86,18 +84,18 @@ struct BuildSDK: ParsableCommand { if let target { buildCommand.append(" --only-target \(target)") } - try zsh(buildCommand, workingDirectoryURL: sdkDirectoryURL) + try Utilities.zsh(buildCommand, workingDirectoryURL: sdkDirectoryURL) } /// Update the Xcode project to use the build of the SDK. func updateXcodeProject() throws { try updateProjectYAML() - try zsh("xcodegen", workingDirectoryURL: projectDirectoryURL) + try Utilities.zsh("xcodegen") } /// Update project.yml with the local path of the SDK. func updateProjectYAML() throws { - let yamlURL = projectDirectoryURL.appending(path: "project.yml") + let yamlURL = Utilities.projectDirectoryURL.appending(path: "project.yml") let yamlString = try String(contentsOf: yamlURL) guard var projectConfig = try Yams.compose(yaml: yamlString) else { throw Error.failureParsingProjectYAML } @@ -106,24 +104,4 @@ struct BuildSDK: ParsableCommand { let updatedYAMLString = try Yams.serialize(node: projectConfig) try updatedYAMLString.write(to: yamlURL, atomically: true, encoding: .utf8) } - - /// Runs a command in zsh. - @discardableResult - func zsh(_ command: String, workingDirectoryURL: URL) throws -> String? { - let process = Process() - process.executableURL = URL(filePath: "/bin/zsh") - process.arguments = ["-c", command] - process.currentDirectoryURL = workingDirectoryURL - - let outputPipe = Pipe() - process.standardOutput = outputPipe - - try process.run() - process.waitUntilExit() - - guard process.terminationReason == .exit, process.terminationStatus == 0 else { throw Error.scriptFailed } - - guard let outputData = try outputPipe.fileHandleForReading.readToEnd() else { return nil } - return String(data: outputData, encoding: .utf8) - } } diff --git a/Tools/Sources/SetupProject.swift b/Tools/Sources/SetupProject.swift new file mode 100644 index 000000000..3d34fa2dd --- /dev/null +++ b/Tools/Sources/SetupProject.swift @@ -0,0 +1,24 @@ +import ArgumentParser +import Foundation + +struct SetupProject: ParsableCommand { + static var configuration = CommandConfiguration(abstract: "A tool to setup the required components to efficiently run and contribute to Element X iOS") + + func run() throws { + try setupGitHooks() + try brewBundleInstall() + try xcodegen() + } + + func setupGitHooks() throws { + try Utilities.zsh("git config core.hooksPath .githooks") + } + + func brewBundleInstall() throws { + try Utilities.zsh("brew bundle install") + } + + func xcodegen() throws { + try Utilities.zsh("xcodegen") + } +} diff --git a/Tools/Sources/Tools.swift b/Tools/Sources/Tools.swift index 40cf0e852..522272363 100644 --- a/Tools/Sources/Tools.swift +++ b/Tools/Sources/Tools.swift @@ -4,5 +4,6 @@ import Foundation @main struct Tools: ParsableCommand { static var configuration = CommandConfiguration(abstract: "A collection of command line tools for ElementX", - subcommands: [BuildSDK.self]) + subcommands: [BuildSDK.self, + SetupProject.self]) } diff --git a/Tools/Sources/Utilities.swift b/Tools/Sources/Utilities.swift new file mode 100644 index 000000000..ead877a48 --- /dev/null +++ b/Tools/Sources/Utilities.swift @@ -0,0 +1,37 @@ +import ArgumentParser +import Foundation + +enum Utilities { + enum Error: LocalizedError { + case scriptFailed(command: String, path: String) + + var errorDescription: String? { + switch self { + case let .scriptFailed(command, path): + return "command \(command) failed in path: \(path)" + } + } + } + + static var projectDirectoryURL: URL { URL(filePath: FileManager.default.currentDirectoryPath) } + + /// Runs a command in zsh. + @discardableResult + static func zsh(_ command: String, workingDirectoryURL: URL = projectDirectoryURL) throws -> String? { + let process = Process() + process.executableURL = URL(filePath: "/bin/zsh") + process.arguments = ["-c", command] + process.currentDirectoryURL = workingDirectoryURL + + let outputPipe = Pipe() + process.standardOutput = outputPipe + + try process.run() + process.waitUntilExit() + + guard process.terminationReason == .exit, process.terminationStatus == 0 else { throw Error.scriptFailed(command: command, path: workingDirectoryURL.absoluteString) } + + guard let outputData = try outputPipe.fileHandleForReading.readToEnd() else { return nil } + return String(data: outputData, encoding: .utf8) + } +}