diff --git a/Calc.xcodeproj/project.pbxproj b/Calc.xcodeproj/project.pbxproj index 0151624..44868aa 100644 --- a/Calc.xcodeproj/project.pbxproj +++ b/Calc.xcodeproj/project.pbxproj @@ -14,6 +14,7 @@ 1A2B3C4D5E6F000100000005 /* KeyboardMonitor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A2B3C4D5E6F000200000005 /* KeyboardMonitor.swift */; }; 1A2B3C4D5E6F000100000006 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 1A2B3C4D5E6F000200000006 /* Assets.xcassets */; }; 1A2B3C4D5E6F000100000007 /* CalculatorEngineTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A2B3C4D5E6F000200000008 /* CalculatorEngineTests.swift */; }; + 1A2B3C4D5E6F000100000008 /* BigDecimal in Frameworks */ = {isa = PBXBuildFile; productRef = 1A2B3C4D5E6F000D00000001 /* BigDecimal */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -43,6 +44,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 1A2B3C4D5E6F000100000008 /* BigDecimal in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -111,6 +113,9 @@ dependencies = ( ); name = Calc; + packageProductDependencies = ( + 1A2B3C4D5E6F000D00000001 /* BigDecimal */, + ); productName = Calc; productReference = 1A2B3C4D5E6F000200000007 /* Calc.app */; productType = "com.apple.product-type.application"; @@ -161,6 +166,9 @@ Base, ); mainGroup = 1A2B3C4D5E6F000700000001; + packageReferences = ( + 1A2B3C4D5E6F000E00000001 /* XCRemoteSwiftPackageReference "BigDecimal" */, + ); productRefGroup = 1A2B3C4D5E6F000700000004 /* Products */; projectDirPath = ""; projectRoot = ""; @@ -220,6 +228,17 @@ }; /* End PBXTargetDependency section */ +/* Begin XCRemoteSwiftPackageReference section */ + 1A2B3C4D5E6F000E00000001 /* XCRemoteSwiftPackageReference "BigDecimal" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/mgriebling/BigDecimal.git"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 3.0.2; + }; + }; +/* End XCRemoteSwiftPackageReference section */ + /* Begin XCBuildConfiguration section */ 1A2B3C4D5E6F000C00000001 /* Debug */ = { isa = XCBuildConfiguration; @@ -444,6 +463,14 @@ defaultConfigurationName = Release; }; /* End XCConfigurationList section */ + +/* Begin XCSwiftPackageProductDependency section */ + 1A2B3C4D5E6F000D00000001 /* BigDecimal */ = { + isa = XCSwiftPackageProductDependency; + package = 1A2B3C4D5E6F000E00000001 /* XCRemoteSwiftPackageReference "BigDecimal" */; + productName = BigDecimal; + }; +/* End XCSwiftPackageProductDependency section */ }; rootObject = 1A2B3C4D5E6F000400000001 /* Project object */; } diff --git a/Calc.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Calc.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved new file mode 100644 index 0000000..d903299 --- /dev/null +++ b/Calc.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -0,0 +1,42 @@ +{ + "originHash" : "883e11126960b9fcbb817a055a5b457b3c3e5426f1977646789c7395fce77b4a", + "pins" : [ + { + "identity" : "bigdecimal", + "kind" : "remoteSourceControl", + "location" : "https://github.com/mgriebling/BigDecimal.git", + "state" : { + "revision" : "4414e0e82bb859cf5a2883f9401e7d7cb030f5b1", + "version" : "3.0.2" + } + }, + { + "identity" : "bigint", + "kind" : "remoteSourceControl", + "location" : "https://github.com/mgriebling/BigInt.git", + "state" : { + "revision" : "f4c0c41dd681b2c3d770b9a83cb5e0e3e30ee708", + "version" : "2.3.0" + } + }, + { + "identity" : "swift-numerics", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-numerics", + "state" : { + "revision" : "0c0290ff6b24942dadb83a929ffaaa1481df04a2", + "version" : "1.1.1" + } + }, + { + "identity" : "uint128", + "kind" : "remoteSourceControl", + "location" : "https://github.com/mgriebling/UInt128.git", + "state" : { + "revision" : "f32052e906fd125936fef2d16e3ad284d50d68ed", + "version" : "3.2.0" + } + } + ], + "version" : 3 +} diff --git a/Calc/CalculatorEngine.swift b/Calc/CalculatorEngine.swift index 9498133..6845392 100644 --- a/Calc/CalculatorEngine.swift +++ b/Calc/CalculatorEngine.swift @@ -1,5 +1,8 @@ +import BigDecimal import Foundation +typealias CalculatorNumber = BigDecimal + enum CalculatorError: LocalizedError, Equatable { case emptyExpression case invalidCharacter(Character) @@ -36,52 +39,163 @@ enum CalculatorError: LocalizedError, Equatable { } struct CalculatorEngine { - private let exponentialThreshold = 1_000_000_000_000.0 + private let minimumWorkingPrecision = 80 + private let guardDigits = 24 + private let displayGroupingSeparator = Locale.current.groupingSeparator ?? "," + private let exponentialDisplayThreshold = BigDecimal("1000000000000") + + func evaluate(_ expression: String) throws -> CalculatorNumber { + let normalized = normalizedExpression(expression) + var parser = ExpressionParser( + expression: normalized, + mathContext: mathContext(for: normalized), + engine: self + ) + return try parser.parse().trim + } - func evaluate(_ expression: String) throws -> Double { - var parser = ExpressionParser(expression: normalizedExpression(expression)) - return try parser.parse() + func format(_ value: CalculatorNumber) -> String { + formatValueForDisplay(value) } - func format(_ value: Double) -> String { - if value.isNaN || value.isInfinite { + func expressionString(_ value: CalculatorNumber) -> String { + guard value.isFinite else { return "Undefined" } - if shouldUseExponentialDisplay(for: value) { - return scientificDisplayString(for: value) + return normalizeScientificNotation(in: value.trim.asString(.plain)) + } + + func formatNumericStringForDisplay(_ value: String) -> String { + let normalized = normalizedExpression(value) + guard !normalized.isEmpty else { return "" } + + if normalized.last == "." { + let integerPart = String(normalized.dropLast()) + return formatPlainNumericString(integerPart) + "." } - if abs(value.rounded() - value) < 1e-10 { - return integerFormatter.string(from: NSNumber(value: value.rounded())) ?? String(Int(value.rounded())) + let number = CalculatorNumber(normalized) + guard number.isFinite else { + return normalized } - return decimalFormatter.string(from: NSNumber(value: value)) ?? String(value) + return formatValueForDisplay(number) } - func expressionString(_ value: Double) -> String { - if value.isNaN || value.isInfinite { + func shouldUseExponentialDisplay(for value: CalculatorNumber) -> Bool { + value.isFinite && value.abs > exponentialDisplayThreshold + } + + func scientificDisplayString(for value: CalculatorNumber) -> String { + guard value.isFinite else { return "Undefined" } - if abs(value.rounded() - value) < 1e-10 { - return integerExpressionFormatter.string(from: NSNumber(value: value.rounded())) ?? String(Int(value.rounded())) + return normalizeScientificNotation(in: value.trim.asString(.scientific)) + } + + fileprivate func mathContext(for expression: String) -> Rounding { + let digitCount = expression.filter(\.isNumber).count + return Rounding(.toNearestOrEven, max(minimumWorkingPrecision, digitCount + guardDigits)) + } + + fileprivate func normalizedExpression(_ expression: String) -> String { + var normalized = expression + + let groupingSeparators = Set([displayGroupingSeparator, ","].filter { !$0.isEmpty }) + for separator in groupingSeparators { + normalized = normalized.replacingOccurrences(of: separator, with: "") } - return decimalFormatter.string(from: NSNumber(value: value)) ?? String(value) + return normalized + .replacingOccurrences(of: "×", with: "*") + .replacingOccurrences(of: "÷", with: "/") + .replacingOccurrences(of: "π", with: "pi") + .replacingOccurrences(of: "√", with: "sqrt") } - func shouldUseExponentialDisplay(for value: Double) -> Bool { - abs(value) > exponentialThreshold + fileprivate func validate(_ value: CalculatorNumber, functionName: String? = nil) throws -> CalculatorNumber { + if value.isNaN { + if let functionName { + throw CalculatorError.domainError(functionName) + } + throw CalculatorError.invalidExpression + } + + if value.isInfinite { + throw CalculatorError.invalidExpression + } + + return value.trim } - func scientificDisplayString(for value: Double) -> String { - let scientific = String(format: "%.6e", locale: Locale(identifier: "en_US_POSIX"), value) - let normalizedScientific = scientific.replacingOccurrences(of: "E", with: "e") - let parts = normalizedScientific.split(separator: "e", maxSplits: 1) + private func formatValueForDisplay(_ value: CalculatorNumber) -> String { + guard value.isFinite else { + return "Undefined" + } + + if shouldUseExponentialDisplay(for: value) { + return scientificDisplayString(for: value) + } + + return formatPlainNumericString(expressionString(value)) + } + + private func formatPlainNumericString(_ value: String) -> String { + var normalized = normalizeScientificNotation(in: value) + + if normalized.lowercased().contains("e") { + let number = CalculatorNumber(normalized) + if shouldUseExponentialDisplay(for: number) { + return scientificDisplayString(for: number) + } + } + + var sign = "" + if normalized.first == "-" { + sign = "-" + normalized.removeFirst() + } else if normalized.first == "+" { + normalized.removeFirst() + } + + let components = normalized.split(separator: ".", maxSplits: 1, omittingEmptySubsequences: false) + let integerPart = components.isEmpty ? "0" : String(components[0]) + let fractionalPart = components.count > 1 ? String(components[1]) : nil + let groupedIntegerPart = groupedIntegerPart(from: integerPart) + + if let fractionalPart { + return sign + groupedIntegerPart + "." + fractionalPart + } + + return sign + groupedIntegerPart + } + + private func groupedIntegerPart(from digits: String) -> String { + let digits = digits.isEmpty ? "0" : digits + guard digits.count > 3 else { return digits } + + let characters = Array(digits) + let firstGroupLength = digits.count % 3 == 0 ? 3 : digits.count % 3 + var grouped = String(characters[0.. String { + let normalized = string.replacingOccurrences(of: "E", with: "e") + let parts = normalized.split(separator: "e", maxSplits: 1, omittingEmptySubsequences: false) guard parts.count == 2, let exponent = Int(parts[1]) else { - return normalizedScientific + return normalized } var mantissa = String(parts[0]) @@ -94,61 +208,21 @@ struct CalculatorEngine { return "\(mantissa)e\(exponent)" } - - private let integerFormatter: NumberFormatter = { - let formatter = NumberFormatter() - formatter.numberStyle = .decimal - formatter.maximumFractionDigits = 0 - return formatter - }() - - private let integerExpressionFormatter: NumberFormatter = { - let formatter = NumberFormatter() - formatter.numberStyle = .decimal - formatter.maximumFractionDigits = 0 - formatter.usesGroupingSeparator = false - return formatter - }() - - private let decimalFormatter: NumberFormatter = { - let formatter = NumberFormatter() - formatter.numberStyle = .decimal - formatter.maximumFractionDigits = 12 - formatter.minimumFractionDigits = 0 - formatter.usesGroupingSeparator = false - return formatter - }() - - private func normalizedExpression(_ expression: String) -> String { - var normalized = expression - - let groupingSeparators = [integerFormatter.groupingSeparator, decimalFormatter.groupingSeparator] - .compactMap { $0 } - .filter { !$0.isEmpty } - - for separator in groupingSeparators { - normalized = normalized.replacingOccurrences(of: separator, with: "") - } - - return normalized - } } private struct ExpressionParser { private let characters: [Character] + private let mathContext: Rounding + private let engine: CalculatorEngine private var index = 0 - init(expression: String) { - self.characters = Array( - expression - .replacingOccurrences(of: "×", with: "*") - .replacingOccurrences(of: "÷", with: "/") - .replacingOccurrences(of: "π", with: "pi") - .replacingOccurrences(of: "√", with: "sqrt") - ) + init(expression: String, mathContext: Rounding, engine: CalculatorEngine) { + self.characters = Array(expression) + self.mathContext = mathContext + self.engine = engine } - mutating func parse() throws -> Double { + mutating func parse() throws -> CalculatorNumber { skipWhitespace() guard !isAtEnd else { @@ -165,7 +239,7 @@ private struct ExpressionParser { return value } - private mutating func parseExpression() throws -> Double { + private mutating func parseExpression() throws -> CalculatorNumber { var value = try parseTerm() while true { @@ -181,7 +255,7 @@ private struct ExpressionParser { } } - private mutating func parseTerm() throws -> Double { + private mutating func parseTerm() throws -> CalculatorNumber { var value = try parseUnary() while true { @@ -191,17 +265,17 @@ private struct ExpressionParser { value *= try parseUnary() } else if match("/") { let divisor = try parseUnary() - guard divisor != 0 else { + guard !divisor.isZero else { throw CalculatorError.divisionByZero } - value /= divisor + value = try engine.validate(value.divide(divisor, mathContext)) } else { return value } } } - private mutating func parseUnary() throws -> Double { + private mutating func parseUnary() throws -> CalculatorNumber { skipWhitespace() if match("+") { @@ -215,19 +289,19 @@ private struct ExpressionParser { return try parsePower() } - private mutating func parsePower() throws -> Double { + private mutating func parsePower() throws -> CalculatorNumber { var value = try parsePrimary() skipWhitespace() if match("^") { let exponent = try parseUnary() - value = Foundation.pow(value, exponent) + value = try engine.validate(BigDecimal.pow(value, exponent, mathContext), functionName: "pow") } return value } - private mutating func parsePrimary() throws -> Double { + private mutating func parsePrimary() throws -> CalculatorNumber { skipWhitespace() if match("(") { @@ -248,21 +322,21 @@ private struct ExpressionParser { return try parseNumber() } - private mutating func parseIdentifierOrFunction() throws -> Double { + private mutating func parseIdentifierOrFunction() throws -> CalculatorNumber { let identifier = parseIdentifier().lowercased() switch identifier { case "pi": - return .pi + return BigDecimal.pi(mathContext) case "e": - return M_E + return try engine.validate(BigDecimal.exp(BigDecimal.one, mathContext)) default: let argument = try parseFunctionArgument() return try evaluateFunction(named: identifier, argument: argument) } } - private mutating func parseNumber() throws -> Double { + private mutating func parseNumber() throws -> CalculatorNumber { skipWhitespace() let start = index @@ -319,7 +393,8 @@ private struct ExpressionParser { throw CalculatorError.malformedNumber } - guard let value = Double(slice) else { + let value = BigDecimal(slice) + guard value.isFinite else { throw CalculatorError.malformedNumber } @@ -341,7 +416,7 @@ private struct ExpressionParser { return String(characters[start.. Double { + private mutating func parseFunctionArgument() throws -> CalculatorNumber { skipWhitespace() if match("(") { @@ -358,45 +433,45 @@ private struct ExpressionParser { return try parseUnary() } - private func evaluateFunction(named name: String, argument: Double) throws -> Double { + private func evaluateFunction(named name: String, argument: CalculatorNumber) throws -> CalculatorNumber { switch name { case "sin": - return Foundation.sin(argument) + return try engine.validate(BigDecimal.sin(argument, mathContext), functionName: "sin") case "cos": - return Foundation.cos(argument) + return try engine.validate(BigDecimal.cos(argument, mathContext), functionName: "cos") case "tan": - return Foundation.tan(argument) + return try engine.validate(BigDecimal.tan(argument, mathContext), functionName: "tan") case "asin": - guard (-1...1).contains(argument) else { + guard argument >= -BigDecimal.one && argument <= BigDecimal.one else { throw CalculatorError.domainError("asin") } - return Foundation.asin(argument) + return try engine.validate(BigDecimal.asin(argument, mathContext), functionName: "asin") case "acos": - guard (-1...1).contains(argument) else { + guard argument >= -BigDecimal.one && argument <= BigDecimal.one else { throw CalculatorError.domainError("acos") } - return Foundation.acos(argument) + return try engine.validate(BigDecimal.acos(argument, mathContext), functionName: "acos") case "atan": - return Foundation.atan(argument) + return try engine.validate(BigDecimal.atan(argument, mathContext), functionName: "atan") case "ln": - guard argument > 0 else { + guard argument > BigDecimal.zero else { throw CalculatorError.domainError("ln") } - return Foundation.log(argument) + return try engine.validate(BigDecimal.log(argument, mathContext), functionName: "ln") case "log", "log10": - guard argument > 0 else { + guard argument > BigDecimal.zero else { throw CalculatorError.domainError("log") } - return Foundation.log10(argument) + return try engine.validate(BigDecimal.log10(argument, mathContext), functionName: "log") case "sqrt": - guard argument >= 0 else { + guard argument >= BigDecimal.zero else { throw CalculatorError.domainError("sqrt") } - return Foundation.sqrt(argument) + return try engine.validate(BigDecimal.sqrt(argument, mathContext), functionName: "sqrt") case "abs": - return Swift.abs(argument) + return argument.abs case "exp": - return Foundation.exp(argument) + return try engine.validate(BigDecimal.exp(argument, mathContext), functionName: "exp") default: throw CalculatorError.invalidFunction(name) } diff --git a/Calc/CalculatorViewModel.swift b/Calc/CalculatorViewModel.swift index 7d1215a..2f27cd8 100644 --- a/Calc/CalculatorViewModel.swift +++ b/Calc/CalculatorViewModel.swift @@ -492,51 +492,7 @@ final class CalculatorViewModel: ObservableObject { private func formatNumberToken(_ token: String) -> String { guard !token.isEmpty else { return "" } - - let normalized = token.replacingOccurrences(of: displayGroupingSeparator, with: "") - - if normalized.lowercased().contains("e"), let value = Double(normalized) { - return engine.scientificDisplayString(for: value) - } - - let components = normalized.split(separator: ".", maxSplits: 1, omittingEmptySubsequences: false) - let integerPart = components.isEmpty ? "0" : String(components[0]) - let fractionalPart = components.count > 1 ? String(components[1]) : nil - - if normalized.last == "." { - let groupedIntegerPart = groupedIntegerPart(from: integerPart) - return groupedIntegerPart + "." - } - - if let value = Double(normalized), engine.shouldUseExponentialDisplay(for: value) { - return engine.scientificDisplayString(for: value) - } - - let groupedIntegerPart = groupedIntegerPart(from: integerPart) - - if let fractionalPart { - return groupedIntegerPart + "." + fractionalPart - } - - return groupedIntegerPart - } - - private func groupedIntegerPart(from digits: String) -> String { - let digits = digits.isEmpty ? "0" : digits - guard digits.count > 3 else { return digits } - - let characters = Array(digits) - let firstGroupLength = digits.count % 3 == 0 ? 3 : digits.count % 3 - var grouped = String(characters[0.. (String, Int)? { diff --git a/CalcTests/CalculatorEngineTests.swift b/CalcTests/CalculatorEngineTests.swift index d7b0074..899ba5c 100644 --- a/CalcTests/CalculatorEngineTests.swift +++ b/CalcTests/CalculatorEngineTests.swift @@ -5,28 +5,42 @@ import XCTest final class CalculatorEngineTests: XCTestCase { private let engine = CalculatorEngine() + private func assertEvaluates(_ expression: String, equals expected: String, file: StaticString = #filePath, line: UInt = #line) throws { + let result = try engine.evaluate(expression) + XCTAssertEqual(engine.expressionString(result), expected, file: file, line: line) + } + + private func assertEvaluates(_ expression: String, approximately expected: Double, accuracy: Double = 0.000001, file: StaticString = #filePath, line: UInt = #line) throws { + let result = try engine.evaluate(expression) + guard let doubleValue = Double(engine.expressionString(result)) else { + return XCTFail("Could not convert result for \(expression) to Double", file: file, line: line) + } + + XCTAssertEqual(doubleValue, expected, accuracy: accuracy, file: file, line: line) + } + func testBasicPrecedence() throws { - XCTAssertEqual(try engine.evaluate("2+3*4"), 14, accuracy: 0.000001) + try assertEvaluates("2+3*4", equals: "14") } func testParentheses() throws { - XCTAssertEqual(try engine.evaluate("2*(3+4)"), 14, accuracy: 0.000001) + try assertEvaluates("2*(3+4)", equals: "14") } func testExponentIsRightAssociative() throws { - XCTAssertEqual(try engine.evaluate("2^3^2"), 512, accuracy: 0.000001) + try assertEvaluates("2^3^2", equals: "512") } func testUnaryMinusAppliesAfterExponent() throws { - XCTAssertEqual(try engine.evaluate("-2^2"), -4, accuracy: 0.000001) + try assertEvaluates("-2^2", equals: "-4") } func testNegativeBaseWithParentheses() throws { - XCTAssertEqual(try engine.evaluate("(-2)^2"), 4, accuracy: 0.000001) + try assertEvaluates("(-2)^2", equals: "4") } func testNegativeExponent() throws { - XCTAssertEqual(try engine.evaluate("2^-3"), 0.125, accuracy: 0.000001) + try assertEvaluates("2^-3", equals: "0.125") } func testDivisionByZeroThrows() { @@ -36,31 +50,39 @@ final class CalculatorEngineTests: XCTestCase { } func testNaturalLogKeyword() throws { - XCTAssertEqual(try engine.evaluate("ln(e)"), 1, accuracy: 0.000001) + try assertEvaluates("ln(e)", approximately: 1) } func testScientificFunctionsAndConstants() throws { - XCTAssertEqual(try engine.evaluate("sin(pi/2)+cos(0)+tan(0)"), 2, accuracy: 0.000001) + try assertEvaluates("sin(pi/2)+cos(0)+tan(0)", approximately: 2) } func testLogAndSquareRoot() throws { - XCTAssertEqual(try engine.evaluate("sqrt(16)+log(1000)"), 7, accuracy: 0.000001) + try assertEvaluates("sqrt(16)+log(1000)", approximately: 7) } func testFunctionWithoutParentheses() throws { - XCTAssertEqual(try engine.evaluate("ln2"), Foundation.log(2), accuracy: 0.000001) + try assertEvaluates("ln2", approximately: Foundation.log(2)) } func testGroupedNumberExpression() throws { - XCTAssertEqual(try engine.evaluate("330,000-260000"), 70000, accuracy: 0.000001) + try assertEvaluates("330,000-260000", equals: "70000") } func testLargeValueFormatsExponentially() { - XCTAssertEqual(engine.format(1_250_000_000_000), "1.25e12") + XCTAssertEqual(engine.format(CalculatorNumber("1250000000000")), "1.25e12") } func testScientificNotationExpressionParses() throws { - XCTAssertEqual(try engine.evaluate("1.25e12-250000000000"), 1_000_000_000_000, accuracy: 0.000001) + try assertEvaluates("1.25e12-250000000000", equals: "1000000000000") + } + + func testArbitraryPrecisionLargeIntegerStaysExact() throws { + try assertEvaluates("999999999999999999999999999999+1", equals: "1000000000000000000000000000000") + } + + func testArbitraryPrecisionDecimalStaysExact() throws { + try assertEvaluates("0.1+0.2", equals: "0.3") } func testUnknownFunctionThrows() { diff --git a/README.md b/README.md index 2c20444..dc12901 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,7 @@ Calc is a macOS SwiftUI calculator app with: +- arbitrary-precision decimal calculations - mouse-friendly circular controls - keyboard input - native copy support for the current display @@ -30,6 +31,8 @@ Use the helper scripts from the repo root: ## Scientific input +Basic arithmetic uses arbitrary-precision decimal math, so large integers and decimal expressions stay exact instead of falling back to `Double` precision. The display still uses grouped digits for readable values and switches to scientific notation for very large results. + Supported scientific functions: - `sin`, `cos`, `tan` @@ -95,11 +98,14 @@ Useful install options: ## Raw Xcode commands -If you want to bypass the helper scripts, these are the equivalent commands. +If you want to bypass the helper scripts, these are the equivalent commands. The `GIT_CONFIG_*` prefix mirrors the helper scripts so Swift package resolution still works on systems that use `safe.bareRepository=explicit`. ### Release build ```bash +GIT_CONFIG_COUNT=1 \ +GIT_CONFIG_KEY_0=safe.bareRepository \ +GIT_CONFIG_VALUE_0=all \ xcodebuild \ -project Calc.xcodeproj \ -scheme Calc \ @@ -113,6 +119,9 @@ xcodebuild \ ### Debug build ```bash +GIT_CONFIG_COUNT=1 \ +GIT_CONFIG_KEY_0=safe.bareRepository \ +GIT_CONFIG_VALUE_0=all \ xcodebuild \ -project Calc.xcodeproj \ -scheme Calc \ @@ -139,6 +148,9 @@ Run the Debug binary directly: ### Tests ```bash +GIT_CONFIG_COUNT=1 \ +GIT_CONFIG_KEY_0=safe.bareRepository \ +GIT_CONFIG_VALUE_0=all \ xcodebuild \ -project Calc.xcodeproj \ -scheme Calc \ diff --git a/scripts/common.sh b/scripts/common.sh index dbc7f8c..a82da72 100755 --- a/scripts/common.sh +++ b/scripts/common.sh @@ -45,5 +45,8 @@ run_xcodebuild() { args+=(CONFIGURATION_BUILD_DIR="$build_dir") fi + GIT_CONFIG_COUNT=1 \ + GIT_CONFIG_KEY_0=safe.bareRepository \ + GIT_CONFIG_VALUE_0=all \ xcodebuild "${args[@]}" "$action" }