#!/usr/bin/env ruby

require 'xcodeproj'

module Xcodeproj
  class Project
    module Object
      # This class represents a custom build rule of a Target.
      #
        class PBXBuildRule < AbstractObject
            attribute :dependency_file, String
        end
    end
  end
end

$projectVersion = "3.7.5"
$macOSDeploymentTarget = "10.14"
$iOSDeploymentTarget = "12.0"

$tests = [
    "Ice/acm",
    "Ice/adapterDeactivation",
    "Ice/admin",
    "Ice/ami",
    "Ice/binding",
    "Ice/defaultServant",
    "Ice/defaultValue",
    "Ice/enums",
    "Ice/exceptions",
    "Ice/facets",
    "Ice/hold",
    "Ice/info",
    "Ice/inheritance",
    "Ice/interceptor",
    "Ice/invoke",
    "Ice/location",
    "Ice/objects",
    "Ice/operations",
    "Ice/optional",
    "Ice/properties",
    "Ice/proxy",
    "Ice/retry",
    "Ice/scope",
    "Ice/servantLocator",
    "Ice/services",
    "Ice/slicing/exceptions",
    "Ice/slicing/objects",
    "Ice/stream",
    "Ice/timeout",
    "Ice/udp",
    "IceSSL/configuration",
    "Slice/escape"
]

$test_variants = ["Client", "Server", "ServerAMD", "Collocated"]

#
# Default sources for each test variant
#
$test_default_sources = {
    "Client" => ["Client.swift", "AllTests.swift", "*Test.ice", "Client.ice"],
    "Server" => ["Server.swift", "TestI.swift", "*Test.ice", "Server.ice"],
    "ServerAMD" => ["ServerAMD.swift", "TestAMDI.swift", "*TestAMD.ice"],
    "Collocated" => ["Collocated.swift"]
}

#
# Extra test sources if any
#
$test_extra_sources = {
    "Ice/objects" => ["Forward.ice"],
    "Ice/operations/Client" => ["BatchOneways.swift",
                                "BatchOnewaysAMI.swift",
                                "Oneways.swift",
                                "OnewaysAMI.swift",
                                "Twoways.swift",
                                "TwowaysAMI.swift"],

    "Ice/servantLocator" => ["ServantLocatorI.swift"],
    "Ice/slicing/exceptions/Client" => ["ClientPrivate.ice"],
    "Ice/slicing/exceptions/Server" => ["ServerPrivate.ice"],
    "Ice/slicing/exceptions/ServerAMD" => ["ServerPrivateAMD.ice"],

    "Ice/slicing/objects/Client" => ["ClientPrivate.ice"],
    "Ice/slicing/objects/Server" => ["ServerPrivate.ice"],
    "Ice/slicing/objects/ServerAMD" => ["ServerPrivateAMD.ice"],
    "Slice/escape" => ["Clash.ice", "Key.ice"]
}

$test_extra_frameworks = {
    "Ice/services" => ["Glacier2.framework", "IceStorm.framework", "IceGrid.framework"]
}

$test_resources = {
    "IceSSL/configuration" => ["../../../../cpp/test/IceSSL/certs"]
}

desc "Generate Xcode projects required to build Ice for Swift"
task :icesdistproj do
    create_project("ice.xcodeproj", false)
end

desc "Generate Xcode projects required to build Ice for Swift test with Carthage binary dist"
task :icebdistproj do
    create_project("ice-test.xcodeproj", true)
end

def create_project(name, bindist)
    project = Xcodeproj::Project.new(name)

    project.build_configurations.each do |config|
        config.build_settings["CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER"] = "YES"
        # Disable header maps as they cause problems with parallel builds on Xcode 12
        config.build_settings["USE_HEADERMAP"] = "NO"
    end

    [:osx, :ios].each do |platform|
        create_platform_targets(project, platform, bindist)
    end

    attributes = project.root_object.attributes
    attributes["TargetAttributes"] ||= {}
    project.targets.each do |target|
        attributes["TargetAttributes"][target.uuid] ||= {}
        attributes["TargetAttributes"][target.uuid]["ProvisioningStyle"] = "Automatic"
    end
    project.root_object.development_region = "en"
    project.root_object.known_regions = ["Base", "en"]

    #
    # Sort the project and save it
    #
    project.sort({:groups_position => :above})
    project.save()
end

task :default => [:icesdistproj]

def create_platform_targets(project, platform, bindist)

    platform_name = platform == :osx ? "macOS" : "iOS"

    unless bindist
        #
        # Ice for C++11 static libraries
        #
        cpp_components = ["Ice", "IceSSL", "IceDiscovery", "IceLocatorDiscovery"]
        cpp_source_dirs = { "Ice" => ["IceUtil", "Ice"] }
        if platform == :ios then
            cpp_components << "IceIAP"
            cpp_source_dirs["Ice"] << "Ice/ios"
        end

        excludes = {
            "Ice" => ["Application.cpp",
                      "AsyncResult.cpp",
                      "AsyncResult.cpp",
                      "ConvertUTF.cpp",
                      "DLLMain.cpp",
                      "GCObject.cpp",
                      "ResponseHandler.cpp",
                      "SystemdJournal.cpp",
                      "Unicode.cpp"],
            "IceSSL" => ["OpenSSL*", "SChannel*", "UWP*"]
        }

        if platform == :ios then
            excludes["Ice"] << "Tcp*"
        end

        cpp_targets = []
        ice_cpp_target = nil

        cpp_components.each do | component |
            target = project.new_target(:static_library, "#{component} C++11 #{platform_name}", platform)
            cpp_targets << target

            group = project_group(project, "slice/#{component}")
            target_add_files(target, group, "../slice/#{component}", ["*.ice"])
            target_add_slice2cpp_build_rule(project, target, component)

            target.build_configurations.each { |config|
                config.build_settings["HEADER_SEARCH_PATHS"] = [
                    "$(SRCROOT)/../cpp/include/",
                    "$(SYMROOT)/$(PLATFORM_NAME)/include/",
                    "$(SRCROOT)/../cpp/src/"
                ]
                config.build_settings["GCC_PREPROCESSOR_DEFINITIONS"] = [
                    "ICE_CPP11_MAPPING",
                    "ICE_BUILDING_SRC",
                    "ICE_STATIC_LIBS",
                    "ICE_SWIFT"
                ]
                if config.name == "Release" then
                    config.build_settings["GCC_PREPROCESSOR_DEFINITIONS"] << "NDEBUG"
                end

                if component == "Ice" then
                    ice_cpp_target = target
                else
                    target.add_dependency(ice_cpp_target)
                end
            }
            target_set_common_build_settings(target, "#{component}++11#{platform_name}", platform)

            source_dirs = cpp_source_dirs[component] || [component]
            source_dirs.each do |d|
                group = project_group(project, "cpp/src/#{d}")
                target_add_files(target, group, "../cpp/src/#{d}", ["*.cpp", "*.mm"], excludes[component] || [])
            end
        end

        #
        # IceImpl framework
        #
        iceimpl_target = project.new_target(:framework, "IceImpl", platform)
        iceimpl_target.name = "IceImpl #{platform_name}"

        target = iceimpl_target
        target.frameworks_build_phases.clear()

        group = project_group(project, "src/IceImpl")
        target_add_files(target, group, "src/IceImpl", ["*.mm"])
        target_add_headers(target, group, "src/IceImpl", ["*.h"],
                           excludes: ["Convert.h", "LoggerWrapperI.h"],
                           attributes: ["Public"])

        target_add_headers(target, group, "src/IceImpl", ["Convert.h", "LoggerWrapperI.h"],
                           attributes: ["Private"])

        target_set_common_build_settings(target, "IceImpl", platform, plist: "src/IceImpl/Info.plist")

        #
        # IceImpl depends on Ice for C++ frameworks
        #
        target.frameworks_build_phases.clear()
        cpp_targets.each do |t|
            target.frameworks_build_phases.add_file_reference(t.product_reference, true)
        end
        target.build_configurations.each { |config|
            config.build_settings["HEADER_SEARCH_PATHS"] = [
                "$(SRCROOT)/../cpp/include/",
                "$(SYMROOT)/$(PLATFORM_NAME)/include/",
                "$(SRCROOT)/../cpp/src/"
            ]
            config.build_settings["DEFINES_MODULE"] = "YES"
            config.build_settings["OTHER_LDFLAGS"] = [
                "-lbz2",
                "-liconv"
            ]
            config.build_settings["GCC_PREPROCESSOR_DEFINITIONS"] = [
                "ICE_CPP11_MAPPING",
                "ICE_STATIC_LIBS",
                "ICE_SWIFT"
            ]

            if config.name == "Release" then
                config.build_settings["GCC_PREPROCESSOR_DEFINITIONS"] << "NDEBUG"
            end
        }
        target_set_framework_build_settings(target)
        target.add_system_framework("Security")
        if platform == :ios then
            target.add_system_framework("ExternalAccessory")
            target.add_system_framework("CFNetwork")
            target.add_system_framework("Foundation")
            target.add_system_framework("UIKit")
        end

        #
        # Ice for Swift framework
        #
        ice_target = project.new_target(:framework, "Ice", platform)
        ice_target.name = "Ice #{platform_name}"

        target = ice_target
        target.frameworks_build_phases.clear()
        target.frameworks_build_phases.add_file_reference(iceimpl_target.product_reference, true)

        target_set_common_build_settings(target, "Ice", platform, plist: "src/Ice/Info.plist", swift: true)

        target.build_configurations.each { |config|
            config.build_settings["DEFINES_MODULE"] = "YES"
        }
        target_set_framework_build_settings(target)
        target_add_carthage_framework(target, platform, "PromiseKit.framework")

        group = project_group(project, "src/Ice")
        group_add_files(group, "src/Ice", ["*.plist"])
        target_add_headers(target, group, "src/Ice", ["*.h"])
        target_add_files(target, group, "src/Ice", ["*.swift"])

        slices = ["Ice", "IceSSL"]
        if platform == :ios then
            slices << "IceIAP"
        end

        slices.each do |item|
            group = project_group(project, "slice/#{item}")
            target_add_files(target, group, "../slice/#{item}", ["*.ice"], excludes[item] || [])
            target_add_slice2swift_build_rule(project, target, item, srcPrefix: "../slice/#{item}")
        end

        target_add_swiftlint_build_phase(target, "src/Ice")

        #
        # Glacier2, IceStorm and IceGrid Frameworks
        #
        glacier2_target = project.new_target(:framework, "Glacier2", platform)
        icestorm_target = project.new_target(:framework, "IceStorm", platform)
        icegrid_target = project.new_target(:framework, "IceGrid", platform)

        framework_targets = {
            "IceImpl.framework" => iceimpl_target,
            "Ice.framework" => ice_target,
            "Glacier2.framework" => glacier2_target,
            "IceStorm.framework" => icestorm_target,
            "IceGrid.framework" => icegrid_target
        }

        [glacier2_target, icestorm_target, icegrid_target].each do | t |
            target = t
            framework = target.name
            target.name = "#{target.name} #{platform_name}"

            target.frameworks_build_phases.clear()

            target_set_common_build_settings(target, framework, platform, plist: "src/#{framework}/Info.plist",
                                             swift: true)

            target.build_configurations.each { |config|
                config.build_settings["DEFINES_MODULE"] = "YES"
            }
            target_set_framework_build_settings(target)
            target_add_carthage_framework(target, platform, "PromiseKit.framework")

            group = project_group(project, "src/#{framework}")
            group_add_files(group, "src/#{framework}", ["*.plist"])
            target_add_headers(target, group, "src/#{framework}", ["*.h"])
            target_add_files(target, group, "src/#{framework}", ["*.swift"])

            group = project_group(project, "slice/#{framework}")
            target_add_files(target, group, "../slice/#{framework}", ["*.ice"])
            target_add_slice2swift_build_rule(project, target, framework, srcPrefix: "../slice/#{framework}")
            target.frameworks_build_phases.add_file_reference(ice_target.product_reference)
            target.add_dependency(ice_target)
        end

        #
        # IceGrid requires Glacier2
        #
        icegrid_target.frameworks_build_phases.add_file_reference(glacier2_target.product_reference)
        icegrid_target.add_dependency(glacier2_target)
    end

    #
    # TestCommon framework
    #
    target = project.new_target(:framework, "TestCommon", platform)
    target.name = "#{target.name} #{platform_name}"
    target_set_framework_build_settings(target)
    target_add_carthage_framework(target, platform, "PromiseKit.framework")
    if bindist then
        target_add_carthage_framework(target, platform, "IceImpl.framework")
        target_add_carthage_framework(target, platform, "Ice.framework")
    else
        target.frameworks_build_phases.add_file_reference(ice_target.product_reference, true)
    end
    target_add_slice2swift_build_rule(project, target, "test/TestCommon", test: true)
    target_add_swiftlint_build_phase(target, "test/TestCommon")
    test_common_target = target

    #
    # Add .ice and .swift files to the target
    #
    group = project_group(project, "test/TestCommon")
    group_add_files(group, "test/TestCommon", ["*.plist"])
    target_add_files(target, group, "test/TestCommon", ["*.ice", "*.swift"])
    target_set_common_build_settings(target, "TestCommon", platform,
                                     plist: "test/TestCommon/Info.plist",
                                     swift: true)

    #
    # TestDriver app
    #
    target = project.new_target(:application, "TestDriver", platform)
    target.name = "#{target.name} #{platform_name}"
    target_set_common_build_settings(target, "TestDriver", platform,
                                     plist: "test/TestDriver/#{platform_name}/Info.plist",
                                     swift: true,
                                     identifier: "com.zeroc.Swift-Test-Controller")
    target_add_slice2swift_build_rule(project, target, "test/TestDriver", srcPrefix: "../scripts", test: true)
    target_add_swiftlint_build_phase(target, "test/TestDriver")

    group = project_group(project, "test/TestDriver")
    target_add_files(target, group, "test/TestDriver", ["*.ice", "*.swift"])

    group = project_group(project, "test/TestDriver/#{platform_name}")
    target_add_files(target, group, "test/TestDriver/#{platform_name}", ["*.ice", "*.swift"])
    if platform == :ios then
        target_add_files(target, group, "../scripts", ["*.ice"])
        target_add_files(target, group, "test/TestDriver/#{platform_name}", ["*.xcassets"])
        target_add_files(target, group, "test/TestDriver/#{platform_name}/Base.lproj", ["*.storyboard"])
        target.add_resources(group_add_files(group, "..", ["certs"]))
        target.build_configurations.each { |config|
            config.build_settings["ONLY_ACTIVE_ARCH"] = "YES"
        }
    end

    #
    # Copy IceImpl, Ice and TestCommon frameworks to the test driver
    #
    copy_phase = target.new_copy_files_build_phase("Copy Frameworks")
    copy_phase.dst_subfolder_spec = Xcodeproj::Constants::COPY_FILES_BUILD_PHASE_DESTINATIONS[:frameworks]

    copy_symbols = target.new_copy_files_build_phase("Copy Symbols")
    copy_symbols.dst_subfolder_spec = Xcodeproj::Constants::COPY_FILES_BUILD_PHASE_DESTINATIONS[:products_directory]

    target_set_framework_build_settings(target)
    if bindist then
        target_add_carthage_framework(target, platform, "IceImpl.framework", true)
        target_add_carthage_framework(target, platform, "Ice.framework", true)
        target_add_carthage_framework(target, platform, "Glacier2.framework", true)
        target_add_carthage_framework(target, platform, "IceStorm.framework", true)
        target_add_carthage_framework(target, platform, "IceGrid.framework", true)
    else
        framework_targets.each do |name, t|
            file = copy_phase.add_file_reference(t.product_reference)
            file.settings = { 'ATTRIBUTES' => ['CodeSignOnCopy'] }
            target.frameworks_build_phases.add_file_reference(t.product_reference)
        end
    end
    file = copy_phase.add_file_reference(test_common_target.product_reference)
    file.settings = { 'ATTRIBUTES' => ['CodeSignOnCopy'] }

    target_add_carthage_framework(target, platform, "PromiseKit.framework", true)

    test_driver_target = target

    #
    # For each test create a bundle target
    #
    $tests.each do |test|
        target = project.new_target(:bundle, target_name("#{test}"), platform)
        target.name = "#{target.name} #{platform_name}"

        if bindist
            target_add_carthage_framework(target, platform, "Ice.framework")
        else
            target.frameworks_build_phases.add_file_reference(ice_target.product_reference, true)
        end
        target.frameworks_build_phases.add_file_reference(test_common_target.product_reference, true)

        extra_frameworks = $test_extra_frameworks[test] || []
        extra_frameworks.each do |f|
            if bindist
                target_add_carthage_framework(target, platform, f)
            else
                if framework_targets.include? f
                    target_framework = framework_targets[f]
                    target.frameworks_build_phases.add_file_reference(target_framework.product_reference, true)
                else
                    target_add_carthage_framework(target, platform, f)
                end
            end
        end

        target_set_framework_build_settings(target)
        target.build_configurations.each { |config|
            config.build_settings["ENABLE_BITCODE"] = "NO"
        }

        target_add_carthage_framework(target, platform, "PromiseKit.framework")
        target_add_slice2swift_build_rule(project, target, "test/#{test}", test: true)
        target_add_swiftlint_build_phase(target, "test/#{test}")

        #
        # Add .ice and .swift files to the target
        #
        group = project_group(project, "test/#{test}")
        group_add_files(group, "test/#{test}", ["*.plist"])

        $test_variants.reject{ |item| item == "ServerAMD" }.each do |variant|
            unless test_has_variant(test, variant)
                next
            end
            target_add_files(target, group, "test/#{test}", test_variant_sources(test, variant))
        end

        if $test_resources.include? test then
            target.add_resources(group_add_files(group, "test/#{test}", $test_resources[test]))
        end

        target_set_common_build_settings(target, target_name("#{test}"), platform,
                                         plist: "test/TestCommon/Info.plist",
                                         swift: true)
        test_driver_target.add_dependency(target)

        #
        # Add the bundle to test driver copy phase
        #
        file = copy_phase.add_file_reference(target.product_reference)
        file.settings = { 'ATTRIBUTES' => ['CodeSignOnCopy'] }
    end

    #
    # Create a separate bundle for AMD test if there is an AMD test variant
    #
    $tests.each do |test|
        if test_has_variant(test, "ServerAMD")
            target = project.new_target(:bundle, target_name("#{test}AMD"), platform)
            target.name = "#{target.name} #{platform_name}"

            if bindist
                target_add_carthage_framework(target, platform, "Ice.framework")
            else
                target.frameworks_build_phases.add_file_reference(ice_target.product_reference, true)
            end

            extra_frameworks = $test_extra_frameworks[test] || []
            extra_frameworks.each do |f|
                if bindist
                    target_add_carthage_framework(target, platform, f)
                else
                    if framework_targets.include? f
                        target_framework = framework_targets[f]
                        target.frameworks_build_phases.add_file_reference(target_framework.product_reference, true)
                    else
                        target_add_carthage_framework(target, platform, f)
                    end
                end
            end
            target.frameworks_build_phases.add_file_reference(test_common_target.product_reference, true)

            target_set_framework_build_settings(target)
            target.build_configurations.each { |config|
                config.build_settings["ENABLE_BITCODE"] = "NO"
            }

            target_add_carthage_framework(target, platform, "PromiseKit.framework")
            target_add_slice2swift_build_rule(project, target, "test/#{test}", test: true)
            target_add_swiftlint_build_phase(target, "test/#{test}")

            #
            # Add .ice and .swift files to the target
            #
            group = project_group(project, "test/#{test}")
            group_add_files(group, "test/#{test}", ["*.plist"])

            target_add_files(target, group, "test/#{test}", test_variant_sources(test, "ServerAMD"),
                             ["Test.ice", "Client*.ice"])
            target_set_common_build_settings(target, target_name("#{test}AMD"), platform,
                                             plist: "test/TestCommon/Info.plist",
                                             swift: true)
            test_driver_target.add_dependency(target)

            #
            # Add the bundle to test driver copy phase
            #
            file = copy_phase.add_file_reference(target.product_reference)
            file.settings = { 'ATTRIBUTES' => ['CodeSignOnCopy'] }
        end
    end

    #
    # Ensure there is a shared scheme to build the frameworks distributed with Carthage builds
    #
    unless bindist
        scheme = Xcodeproj::XCScheme.new
        scheme.add_build_target(iceimpl_target)
        scheme.add_build_target(ice_target)
        scheme.add_build_target(glacier2_target)
        scheme.add_build_target(icestorm_target)
        scheme.add_build_target(icegrid_target)
        scheme.save_as("ice.xcodeproj", "Ice #{platform_name}", true)
    end
end

def project_group(project, name)
    group = project.main_group
    name.split("/").each { |item|
        new_group = group[item]
        unless new_group
            new_group = group.new_group(item)
        end
        group = new_group
    }
    group
end

def target_name(basename, suffix = nil)
    name = basename.split("/").map{ |item| item[0].upcase + item[1..-1]}.join()
    suffix ? "#{name} #{suffix}" : name
end

def target_add_slice2swift_build_rule(project, target, projectPrefix, srcPrefix: nil, test: false)
    unless srcPrefix
        srcPrefix = projectPrefix
    end
    #
    # Add Slice Compiler build rule to the target
    #
    rule = project.new(Xcodeproj::Project::PBXBuildRule)
    rule.compiler_spec = "com.apple.compilers.proxy.script"
    rule.file_type = "pattern.proxy"

    slic2swift = <<~EOF
        if [ -f "$ICE_HOME/cpp/bin/slice2swift" ]; then
            SLICE2SWIFT="$ICE_HOME/cpp/bin/slice2swift"
        elif [ -f "$SRCROOT/../cpp/bin/slice2swift" ]; then
            SLICE2SWIFT="$SRCROOT/../cpp/bin/slice2swift"
        elif [ -f /usr/local/bin/slice2swift ]; then
            SLICE2SWIFT=/usr/local/bin/slice2swift
        else
            echo "Failed to locate slice2swift compiler"
            exit 1
        fi
    EOF

    rule.name = "Slice Compiler for #{projectPrefix}/*.ice"
    rule.run_once_per_architecture = "0"
    rule.file_patterns = "*/#{srcPrefix.delete_prefix('../')}/*.ice"

    if test then
        rule.script = <<~EOF
            #{slic2swift}
            "$SLICE2SWIFT" -I"$SRCROOT/../slice" -I"$INPUT_FILE_DIR" \
                --depend --depend-file "$DERIVED_FILE_DIR/$INPUT_FILE_BASE.d" "$INPUT_FILE_PATH"

            echo "$INPUT_FILE_BASE.ice: $SLICE2SWIFT" >> "$DERIVED_FILE_DIR/$INPUT_FILE_BASE.d"

            "$SLICE2SWIFT" -I"$SRCROOT/../slice" -I"$INPUT_FILE_DIR" --output-dir "$DERIVED_FILE_DIR" "$INPUT_FILE_PATH"
        EOF
        rule.input_files = find_files(srcPrefix, ["*.ice"]).map { |p| "#{srcPrefix}/#{p}" }
        rule.output_files = ["$(DERIVED_FILE_DIR)/$(INPUT_FILE_BASE).swift"]
        rule.dependency_file = "$(DERIVED_FILE_DIR)/$(INPUT_FILE_BASE).d"
    else
        rule.script = <<~EOF
            #{slic2swift}
            BASENAME=$(basename -- "$INPUT_FILE_PATH")
            BASENAME="${BASENAME%.*}"
            mkdir -p "$DERIVED_FILE_DIR/#{projectPrefix}"
            "$SLICE2SWIFT" -I"$SRCROOT/../slice" -I"$INPUT_FILE_DIR" \
                --depend --depend-file "$DERIVED_FILE_DIR/#{projectPrefix}_$INPUT_FILE_BASE.d" "$INPUT_FILE_PATH"

            echo "$INPUT_FILE_BASE.ice: $SLICE2SWIFT" >> "$DERIVED_FILE_DIR/#{projectPrefix}_$INPUT_FILE_BASE.d"

            "$SLICE2SWIFT" -I"$SRCROOT/../slice" -I"$INPUT_FILE_DIR" --output-dir "$DERIVED_FILE_DIR/#{projectPrefix}" \
                "$INPUT_FILE_PATH"
            mv "$DERIVED_FILE_DIR/#{projectPrefix}/$BASENAME.swift" "$DERIVED_FILE_DIR/#{projectPrefix}_$BASENAME.swift"
        EOF
        rule.input_files = find_files(srcPrefix, ["*.ice"]).map { |p| "$SRCROOT/#{srcPrefix}/#{p}" }
        rule.output_files = ["$(DERIVED_FILE_DIR)/#{projectPrefix}_$(INPUT_FILE_BASE).swift"]
        rule.dependency_file = "$(DERIVED_FILE_DIR)/#{projectPrefix}_$(INPUT_FILE_BASE).d"
    end
    target.build_rules << rule
end

def target_add_slice2cpp_build_rule(project, target, prefix)
    #
    # Add Slice Compiler build rule to the target
    #
    rule = project.new(Xcodeproj::Project::PBXBuildRule)
    rule.compiler_spec = "com.apple.compilers.proxy.script"
    rule.run_once_per_architecture = "0"
    rule.file_type = "pattern.proxy"
    rule.name = "Slice2Cpp Compiler for #{prefix}/*.ice"
    rule.file_patterns = "*/#{prefix}/*.ice"
    rule.script = <<~EOF
        if [ -f "$ICE_HOME/cpp/bin/slice2cpp" ]; then
            SLICE2CPP="$ICE_HOME/cpp/bin/slice2cpp"
        elif [ -f "$SRCROOT/../cpp/bin/slice2cpp" ]; then
            SLICE2CPP="$SRCROOT/../cpp/bin/slice2cpp"
        elif [ -f /usr/local/opt/slice2swift/libexec/bin/slice2cpp ]; then
            SLICE2CPP=/usr/local/opt/slice2swift/libexec/bin/slice2cpp
        elif [ -f /usr/local/bin/slice2cpp ]; then
            SLICE2CPP=/usr/local/bin/slice2cpp
        else
            echo "Failed to locate slice2cpp compiler"
            exit 1
        fi

        BASENAME=$(basename -- "$INPUT_FILE_PATH")
        BASENAME="${BASENAME%.*}"
        mkdir -p "$DERIVED_FILE_DIR/#{prefix}"
        $SLICE2CPP -I"$SRCROOT/../slice" -D ICE_SWIFT --include-dir #{prefix} \
            --output-dir "$DERIVED_FILE_DIR/#{prefix}" \
            --depend --depend-file "$DERIVED_FILE_DIR/#{prefix}/$INPUT_FILE_BASE.d" \
            "$INPUT_FILE_PATH"

        echo "$INPUT_FILE_BASE.ice: $SLICE2CPP" >> "$DERIVED_FILE_DIR/#{prefix}/$INPUT_FILE_BASE.d"

        $SLICE2CPP -I"$SRCROOT/../slice" -D ICE_SWIFT --include-dir #{prefix} \
            --output-dir "$DERIVED_FILE_DIR/#{prefix}" \
            "$INPUT_FILE_PATH"
        mkdir -p "$SYMROOT/$PLATFORM_NAME/include/#{prefix}"
        mv "$DERIVED_FILE_DIR/#{prefix}/$BASENAME.h" "$SYMROOT/$PLATFORM_NAME/include/#{prefix}/$BASENAME.h"
    EOF
    rule.output_files = ["$(DERIVED_FILE_DIR)/#{prefix}/$(INPUT_FILE_BASE).cpp",
                         "$(SYMROOT)/$(PLATFORM_NAME)/include/#{prefix}/$(INPUT_FILE_BASE).h"]
    inputs = find_files("../slice/#{prefix}", ["*.ice"]).map { |p| File.basename(p, ".ice") }
    rule.input_files = inputs.map { |p|
        "$SRCROOT/../slice/#{prefix}/#{p}.ice"
    }
    rule.dependency_file= "$(DERIVED_FILE_DIR)/#{prefix}/$(INPUT_FILE_BASE).d"
    target.build_rules << rule
end

def target_add_swiftlint_build_phase(target, basedir)
    phase = target.new_shell_script_build_phase("Swiftformat & Swiftlint")
    phase.shell_script = <<~EOF
        if which swiftlint >/dev/null; then
            swiftlint --path "$SRCROOT/#{basedir}" --config "$SRCROOT/.swiftlint.yml"
        else
            echo "warning: SwiftLint not installed, download from https://github.com/realm/SwiftLint"
        fi
    EOF
end

def find_files(basedir, patterns, exclude = [])
    files = []
    Dir.chdir(basedir) do
        patterns.each do |p|
            Dir.glob(p) do |file|
                files << file
            end
        end
    end
    return files.reject { |item| exclude.any? { |pattern| item.match(pattern) } }
end

def group_add_files(group, basedir, patterns, exclude = [])
    find_files(basedir, patterns, exclude).map { |file|
        group.find_subpath(File.basename(file)) ||  group.new_file("#{basedir}/#{file}") }
end

def target_add_files(target, group, basedir, patterns, excludes = [])
    target.add_file_references(group_add_files(group, basedir, patterns, excludes))
end

def target_add_headers(target, group, basedir, patterns, excludes: [], attributes: ["Public"])
    files = group_add_files(group, basedir, patterns, excludes)
    files.each do |file|
        header = target.headers_build_phase.add_file_reference(file)
        header.settings = { "ATTRIBUTES" => attributes }
    end
end

def target_set_common_build_settings(target, product, platform, plist: nil, swift: false, identifier: nil)
    target.build_configurations.each { |config|
        config.build_settings["GCC_SYMBOLS_PRIVATE_EXTERN"] = "YES"
        config.build_settings["ENABLE_TESTABILITY"] = "NO"
        config.build_settings["GCC_TREAT_WARNINGS_AS_ERRORS"] = "YES"
        config.build_settings["CURRENT_PROJECT_VERSION"] = $projectVersion
        config.build_settings["DYLIB_CURRENT_VERSION"] = $projectVersion
        config.build_settings["DYLIB_COMPATIBILITY_VERSION"] = "0"

        if plist then
            config.build_settings["INFOPLIST_FILE"] = plist
        end
        config.build_settings["PRODUCT_NAME"] = product
        config.build_settings["PRODUCT_BUNDLE_IDENTIFIER"] = identifier || "com.zeroc.#{product}"

        if swift then
            config.build_settings["SWIFT_VERSION"] = "5.2"
            config.build_settings["SWIFT_TREAT_WARNINGS_AS_ERRORS"] = "YES"
        end

        config.build_settings["SUPPORTED_PLATFORMS"] =
            (target.name.include? "macOS") ? "macosx" : "iphoneos iphonesimulator"

        config.build_settings["AVAILABLE_PLATFORMS"] =
            (target.name.include? "macOS") ? "macosx" : "iphoneos iphonesimulator"

        config.build_settings["CODE_SIGN_STYLE"] = "Automatic"
        if config.name == "Debug" or target.product_type == "com.apple.product-type.application" then
            config.build_settings["CODE_SIGN_IDENTITY"] = "-";
        else
            config.build_settings["CODE_SIGN_IDENTITY"] = "";
        end

        if platform == :osx
            config.build_settings["MACOSX_DEPLOYMENT_TARGET"] = $macOSDeploymentTarget
        else
            config.build_settings["IPHONEOS_DEPLOYMENT_TARGET"] = $iOSDeploymentTarget
        end
    }
end

def target_set_framework_build_settings(target)
    carthage_prefix = (target.name.include? "macOS") ? "Mac" : "iOS"
    target.build_configurations.each { |config|
        config.build_settings["FRAMEWORK_SEARCH_PATHS"] = "$(SRCROOT)/../Carthage/Build/#{carthage_prefix}"
    }
end

def target_add_carthage_framework(target, platform, framework, copy=false)
    group = target.project.frameworks_group[(platform == :osx ? "OS X" : "iOS")]
    carthage_prefix = (platform == :osx) ? "Mac" : "iOS"
    group_add_files(group, "../Carthage/Build/#{carthage_prefix}", [framework]).each do |ref|
        # When copy is true we copy the framework to the target, for :ios targets we use the carthage copy-frameworks
        # script for macOS targets we use the regular copy phase.
        if copy then
            if platform == :ios then
                copy_phase = target.shell_script_build_phases.select { |script| script.name == "Copy Frameworks" }.first
                unless copy_phase then
                    copy_phase = target.new_shell_script_build_phase("Copy Frameworks")
                    copy_phase.shell_script = "/usr/local/bin/carthage copy-frameworks"
                end
                copy_phase.input_paths << "$(SRCROOT)/../Carthage/Build/#{carthage_prefix}/#{framework}"
                copy_phase.output_paths << "$(BUILT_PRODUCTS_DIR)/$(FRAMEWORKS_FOLDER_PATH)/#{framework}"
            else
                file = target.copy_files_build_phases[0].add_file_reference(ref)
                file.settings = { 'ATTRIBUTES' => ['CodeSignOnCopy'] }
            end
        end

        # when copy is false or the target platform is ios we also have to add a reference into the target
        # frameworks build phase.
        unless copy and platform == :macos then
            target.frameworks_build_phases.add_file_reference(ref, true)
        end
    end
end

#
# Check if the test include the given variant
#
def test_has_variant(test, variant)
    return File.file?("test/#{test}/#{variant}.swift")
end

def test_variant_sources(test, variant)
    sources = $test_default_sources[variant]

    if variant == "Collocated"
        sources += $test_default_sources["Server"].reject { |s| s == "Server.swift" }
        sources += $test_default_sources["Client"].reject { |s| s == "Client.swift" }

        sources += $test_extra_sources["#{test}/Server"] || []
        sources += $test_extra_sources["#{test}/Client"] || []
    end

    sources += $test_extra_sources[test] || []
    sources += $test_extra_sources["#{test}/#{variant}"] || []

    return sources
end
