/****************************************************************************
**
** Copyright (C) 2015 The Qt Company Ltd.
** Contact: http://www.qt.io/licensing
**
** This file is part of Qbs.
**
** Commercial License Usage
** Licensees holding valid commercial Qt licenses may use this file in
** accordance with the commercial license agreement provided with the
** Software or, alternatively, in accordance with the terms contained in
** a written agreement between you and The Qt Company. For licensing terms and
** conditions see http://www.qt.io/terms-conditions. For further information
** use the contact form at http://www.qt.io/contact-us.
**
** GNU Lesser General Public License Usage
** Alternatively, this file may be used under the terms of the GNU Lesser
** General Public License version 2.1 or version 3 as published by the Free
** Software Foundation and appearing in the file LICENSE.LGPLv21 and
** LICENSE.LGPLv3 included in the packaging of this file.  Please review the
** following information to ensure the GNU Lesser General Public License
** requirements will be met: https://www.gnu.org/licenses/lgpl.html and
** http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html.
**
** In addition, as a special exception, The Qt Company gives you certain additional
** rights.  These rights are described in The Qt Company LGPL Exception
** version 1.1, included in the file LGPL_EXCEPTION.txt in this package.
**
****************************************************************************/

import qbs.BundleTools
import qbs.DarwinTools
import qbs.Environment
import qbs.File
import qbs.FileInfo
import qbs.ModUtils
import qbs.PropertyList
import qbs.TextFile
import qbs.Utilities
import "bundle.js" as Bundle

Module {
    Depends { name: "xcode"; required: false; }

    Probe {
        id: bundleSettingsProbe
        condition: qbs.targetOS.contains("darwin")

        property string xcodeDeveloperPath: xcode.developerPath
        property var xcodeArchSettings: xcode._architectureSettings
        property string productTypeIdentifier: _productTypeIdentifier
        property bool useXcodeBuildSpecs: _useXcodeBuildSpecs
        property bool isMacOs: qbs.targetOS.contains("macos")
        property bool xcodePresent: xcode.present

        // Note that we include several settings pointing to properties which reference the output
        // of this probe (WRAPPER_NAME, WRAPPER_EXTENSION, etc.). This is to ensure that derived
        // properties take into account the value of these settings if the user customized them.
        property var additionalSettings: ({
            "DEVELOPMENT_LANGUAGE": "English",
            "EXECUTABLE_VARIANT_SUFFIX": "", // e.g. _debug, _profile
            "FRAMEWORK_VERSION": frameworkVersion,
            "GENERATE_PKGINFO_FILE": generatePackageInfo !== undefined
                                     ? (generatePackageInfo ? "YES" : "NO")
                                     : undefined,
            "IS_MACCATALYST": "NO",
            "LD_RUNPATH_SEARCH_PATHS_NO": [],
            "PRODUCT_NAME": product.targetName,
            "LOCAL_APPS_DIR": Environment.getEnv("HOME") + "/Applications",
            "LOCAL_LIBRARY_DIR": Environment.getEnv("HOME") + "/Library",
            "SWIFT_PLATFORM_TARGET_PREFIX": isMacOs ? "macos"
                                                    : qbs.targetOS.contains("ios") ? "ios" : "",
            "TARGET_BUILD_DIR": product.buildDirectory,
            "WRAPPER_NAME": bundleName,
            "WRAPPER_EXTENSION": extension
        })

        // Outputs
        property var xcodeSettings: ({})
        property var productTypeIdentifierChain: []

        configure: {
            var specsPath = path;
            var specsSeparator = "-";
            if (xcodeDeveloperPath && useXcodeBuildSpecs) {
                specsPath = xcodeDeveloperPath
                        + "/Platforms/MacOSX.platform/Developer/Library/Xcode/Specifications";
                specsSeparator = " ";
            }

            var reader = new Bundle.XcodeBuildSpecsReader(specsPath,
                                                          specsSeparator,
                                                          additionalSettings,
                                                          !isMacOs);
            var settings = reader.expandedSettings(productTypeIdentifier,
                                                   xcodePresent
                                                   ? xcodeArchSettings
                                                   : {});
            var chain = reader.productTypeIdentifierChain(productTypeIdentifier);
            if (settings && chain) {
                xcodeSettings = settings;
                productTypeIdentifierChain = chain;
                found = true;
            } else {
                xcodeSettings = {};
                productTypeIdentifierChain = [];
                found = false;
            }
        }
    }

    additionalProductTypes: !(product.multiplexed || product.aggregate)
                            || !product.multiplexConfigurationId ? ["bundle.content"] : []

    property bool isBundle: !product.consoleApplication && qbs.targetOS.contains("darwin")

    readonly property bool isShallow: bundleSettingsProbe.xcodeSettings["SHALLOW_BUNDLE"] === "YES"

    property string identifierPrefix: "org.example"
    property string identifier: [identifierPrefix, Utilities.rfc1034Identifier(product.targetName)].join(".")

    property string extension: bundleSettingsProbe.xcodeSettings["WRAPPER_EXTENSION"]

    property string packageType: Bundle.packageType(_productTypeIdentifier)

    property string signature: "????" // legacy creator code in Mac OS Classic (CFBundleSignature), can be ignored

    property string bundleName: bundleSettingsProbe.xcodeSettings["WRAPPER_NAME"]

    property string frameworkVersion: {
        var n = parseInt(product.version, 10);
        return isNaN(n) ? bundleSettingsProbe.xcodeSettings["FRAMEWORK_VERSION"] : String(n);
    }

    property bool generatePackageInfo: {
        // Make sure to return undefined as default to indicate "not set"
        var genPkgInfo = bundleSettingsProbe.xcodeSettings["GENERATE_PKGINFO_FILE"];
        if (genPkgInfo)
            return genPkgInfo === "YES";
    }

    property pathList publicHeaders
    property pathList privateHeaders
    property pathList resources

    property var infoPlist
    property bool processInfoPlist: true
    property bool embedInfoPlist: product.consoleApplication && !isBundle
    property string infoPlistFormat: qbs.targetOS.contains("macos") ? "same-as-input" : "binary1"

    property string localizedResourcesFolderSuffix: ".lproj"

    property string lsregisterName: "lsregister"
    property string lsregisterPath: FileInfo.joinPaths(
                                        "/System/Library/Frameworks/CoreServices.framework" +
                                        "/Versions/A/Frameworks/LaunchServices.framework" +
                                        "/Versions/A/Support", lsregisterName);

    // all paths are relative to the directory containing the bundle
    readonly property string infoPlistPath: bundleSettingsProbe.xcodeSettings["INFOPLIST_PATH"]
    readonly property string infoStringsPath: bundleSettingsProbe.xcodeSettings["INFOSTRINGS_PATH"]
    readonly property string pbdevelopmentPlistPath: bundleSettingsProbe.xcodeSettings["PBDEVELOPMENTPLIST_PATH"]
    readonly property string pkgInfoPath: bundleSettingsProbe.xcodeSettings["PKGINFO_PATH"]
    readonly property string versionPlistPath: bundleSettingsProbe.xcodeSettings["VERSIONPLIST_PATH"]

    readonly property string executablePath: bundleSettingsProbe.xcodeSettings["EXECUTABLE_PATH"]

    readonly property string contentsFolderPath: bundleSettingsProbe.xcodeSettings["CONTENTS_FOLDER_PATH"]
    readonly property string documentationFolderPath: bundleSettingsProbe.xcodeSettings["DOCUMENTATION_FOLDER_PATH"]
    readonly property string executableFolderPath: bundleSettingsProbe.xcodeSettings["EXECUTABLE_FOLDER_PATH"]
    readonly property string executablesFolderPath: bundleSettingsProbe.xcodeSettings["EXECUTABLES_FOLDER_PATH"]
    readonly property string frameworksFolderPath: bundleSettingsProbe.xcodeSettings["FRAMEWORKS_FOLDER_PATH"]
    readonly property string javaFolderPath: bundleSettingsProbe.xcodeSettings["JAVA_FOLDER_PATH"]
    readonly property string localizedResourcesFolderPath: bundleSettingsProbe.xcodeSettings["LOCALIZED_RESOURCES_FOLDER_PATH"]
    readonly property string pluginsFolderPath: bundleSettingsProbe.xcodeSettings["PLUGINS_FOLDER_PATH"]
    readonly property string privateHeadersFolderPath: bundleSettingsProbe.xcodeSettings["PRIVATE_HEADERS_FOLDER_PATH"]
    readonly property string publicHeadersFolderPath: bundleSettingsProbe.xcodeSettings["PUBLIC_HEADERS_FOLDER_PATH"]
    readonly property string scriptsFolderPath: bundleSettingsProbe.xcodeSettings["SCRIPTS_FOLDER_PATH"]
    readonly property string sharedFrameworksFolderPath: bundleSettingsProbe.xcodeSettings["SHARED_FRAMEWORKS_FOLDER_PATH"]
    readonly property string sharedSupportFolderPath: bundleSettingsProbe.xcodeSettings["SHARED_SUPPORT_FOLDER_PATH"]
    readonly property string unlocalizedResourcesFolderPath: bundleSettingsProbe.xcodeSettings["UNLOCALIZED_RESOURCES_FOLDER_PATH"]
    readonly property string versionsFolderPath: bundleSettingsProbe.xcodeSettings["VERSIONS_FOLDER_PATH"]

    // private properties
    property string _productTypeIdentifier: Bundle.productTypeIdentifier(product.type)
    property stringList _productTypeIdentifierChain: bundleSettingsProbe.productTypeIdentifierChain

    property bool _useXcodeBuildSpecs: true // false to use ONLY the qbs build specs

    readonly property var extraEnv: ({
        "PRODUCT_BUNDLE_IDENTIFIER": identifier
    })

    readonly property var qmakeEnv: {
        return {
            "BUNDLEIDENTIFIER": identifier,
            "EXECUTABLE": product.targetName,
            "FULL_VERSION": product.version || "1.0", // CFBundleVersion
            "ICON": product.targetName, // ### QBS-73
            "LIBRARY": product.targetName,
            "SHORT_VERSION": product.version || "1.0", // CFBundleShortVersionString
            "TYPEINFO": signature // CFBundleSignature
        };
    }

    readonly property var defaultInfoPlist: {
        return {
            CFBundleDevelopmentRegion: "en", // default localization
            CFBundleDisplayName: product.targetName, // localizable
            CFBundleExecutable: product.targetName,
            CFBundleIdentifier: identifier,
            CFBundleInfoDictionaryVersion: "6.0",
            CFBundleName: product.targetName, // short display name of the bundle, localizable
            CFBundlePackageType: packageType,
            CFBundleShortVersionString: product.version || "1.0", // "release" version number, localizable
            CFBundleSignature: signature, // legacy creator code in Mac OS Classic, can be ignored
            CFBundleVersion: product.version || "1.0.0" // build version number, must be 3 octets
        };
    }

    validate: {
        if (!qbs.targetOS.contains("darwin"))
            return;
        if (!bundleSettingsProbe.found) {
            var error = "Bundle product type " + _productTypeIdentifier + " is not supported.";
            if ((_productTypeIdentifier || "").startsWith("com.apple.product-type."))
                error += " You may need to upgrade Xcode.";
            throw error;
        }

        var validator = new ModUtils.PropertyValidator("bundle");
        validator.setRequiredProperty("bundleName", bundleName);
        validator.setRequiredProperty("infoPlistPath", infoPlistPath);
        validator.setRequiredProperty("pbdevelopmentPlistPath", pbdevelopmentPlistPath);
        validator.setRequiredProperty("pkgInfoPath", pkgInfoPath);
        validator.setRequiredProperty("versionPlistPath", versionPlistPath);
        validator.setRequiredProperty("executablePath", executablePath);
        validator.setRequiredProperty("contentsFolderPath", contentsFolderPath);
        validator.setRequiredProperty("documentationFolderPath", documentationFolderPath);
        validator.setRequiredProperty("executableFolderPath", executableFolderPath);
        validator.setRequiredProperty("executablesFolderPath", executablesFolderPath);
        validator.setRequiredProperty("frameworksFolderPath", frameworksFolderPath);
        validator.setRequiredProperty("javaFolderPath", javaFolderPath);
        validator.setRequiredProperty("localizedResourcesFolderPath", localizedResourcesFolderPath);
        validator.setRequiredProperty("pluginsFolderPath", pluginsFolderPath);
        validator.setRequiredProperty("privateHeadersFolderPath", privateHeadersFolderPath);
        validator.setRequiredProperty("publicHeadersFolderPath", publicHeadersFolderPath);
        validator.setRequiredProperty("scriptsFolderPath", scriptsFolderPath);
        validator.setRequiredProperty("sharedFrameworksFolderPath", sharedFrameworksFolderPath);
        validator.setRequiredProperty("sharedSupportFolderPath", sharedSupportFolderPath);
        validator.setRequiredProperty("unlocalizedResourcesFolderPath", unlocalizedResourcesFolderPath);

        if (packageType === "FMWK") {
            validator.setRequiredProperty("frameworkVersion", frameworkVersion);
            validator.setRequiredProperty("versionsFolderPath", versionsFolderPath);
        }

        // extension and infoStringsPath might not be set
        return validator.validate();
    }

    FileTagger {
        fileTags: ["infoplist"]
        patterns: ["Info.plist", "*-Info.plist"]
    }

    Rule {
        condition: qbs.targetOS.contains("darwin")
        multiplex: true
        requiresInputs: false // TODO: The resources property should probably be a tag instead.
        inputs: ["infoplist", "partial_infoplist"]

        outputFileTags: ["bundle.input", "aggregate_infoplist"]
        outputArtifacts: {
            var artifacts = [];
            var embed = ModUtils.moduleProperty(product, "embedInfoPlist");
            if (ModUtils.moduleProperty(product, "isBundle") || embed) {
                artifacts.push({
                    filePath: FileInfo.joinPaths(
                                  product.destinationDirectory, product.name + "-Info.plist"),
                    fileTags: ["aggregate_infoplist"].concat(!embed ? ["bundle.input"] : []),
                    bundle: {
                        _bundleFilePath: FileInfo.joinPaths(
                                             product.destinationDirectory,
                                             ModUtils.moduleProperty(product, "infoPlistPath")),
                    }
                });
            }
            return artifacts;
        }

        prepare: {
            var cmd = new JavaScriptCommand();
            cmd.description = "generating Info.plist for " + product.name;
            cmd.highlight = "codegen";
            cmd.infoPlist = ModUtils.moduleProperty(product, "infoPlist") || {};
            cmd.processInfoPlist = ModUtils.moduleProperty(product, "processInfoPlist");
            cmd.infoPlistFormat = ModUtils.moduleProperty(product, "infoPlistFormat");
            cmd.extraEnv = ModUtils.moduleProperty(product, "extraEnv");
            cmd.qmakeEnv = ModUtils.moduleProperty(product, "qmakeEnv");

            cmd.buildEnv = product.moduleProperty("cpp", "buildEnv");

            cmd.developerPath = product.moduleProperty("xcode", "developerPath");
            cmd.platformInfoPlist = product.moduleProperty("xcode", "platformInfoPlist");
            cmd.sdkSettingsPlist = product.moduleProperty("xcode", "sdkSettingsPlist");
            cmd.toolchainInfoPlist = product.moduleProperty("xcode", "toolchainInfoPlist");

            cmd.osBuildVersion = product.moduleProperty("qbs", "hostOSBuildVersion");

            cmd.sourceCode = function() {
                var plist, process, key, i;

                // Contains the combination of default, file, and in-source keys and values
                // Start out with the contents of this file as the "base", if given
                var aggregatePlist = {};

                for (i = 0; i < (inputs.infoplist || []).length; ++i) {
                    aggregatePlist =
                            BundleTools.infoPlistContents(inputs.infoplist[i].filePath);
                    infoPlistFormat = (infoPlistFormat === "same-as-input")
                            ? BundleTools.infoPlistFormat(inputs.infoplist[i].filePath)
                            : "xml1";
                    break;
                }

                // Add local key-value pairs (overrides equivalent keys specified in the file if
                // one was given)
                for (key in infoPlist) {
                    if (infoPlist.hasOwnProperty(key))
                        aggregatePlist[key] = infoPlist[key];
                }

                // Do some postprocessing if desired
                if (processInfoPlist) {
                    // Add default values to the aggregate plist if the corresponding keys
                    // for those values are not already present
                    var defaultValues = ModUtils.moduleProperty(product, "defaultInfoPlist");
                    for (key in defaultValues) {
                        if (defaultValues.hasOwnProperty(key) && !(key in aggregatePlist))
                            aggregatePlist[key] = defaultValues[key];
                    }

                    // Add keys from platform's Info.plist if not already present
                    var platformInfo = {};
                    var sdkSettings = {};
                    var toolchainInfo = {};
                    if (developerPath) {
                        plist = new PropertyList();
                        try {
                            plist.readFromFile(platformInfoPlist);
                            platformInfo = plist.toObject();
                        } finally {
                            plist.clear();
                        }

                        var additionalProps = platformInfo["AdditionalInfo"];
                        for (key in additionalProps) {
                            // override infoPlist?
                            if (additionalProps.hasOwnProperty(key) && !(key in aggregatePlist))
                                aggregatePlist[key] = defaultValues[key];
                        }
                        props = platformInfo['OverrideProperties'];
                        for (key in props) {
                            aggregatePlist[key] = props[key];
                        }

                        plist = new PropertyList();
                        try {
                            plist.readFromFile(sdkSettingsPlist);
                            sdkSettings = plist.toObject();
                        } finally {
                            plist.clear();
                        }

                        plist = new PropertyList();
                        try {
                            plist.readFromFile(toolchainInfoPlist);
                            toolchainInfo = plist.toObject();
                        } finally {
                            plist.clear();
                        }
                    }

                    aggregatePlist["BuildMachineOSBuild"] = osBuildVersion;

                    // setup env
                    env = {
                        "SDK_NAME": sdkSettings["CanonicalName"],
                        "XCODE_VERSION_ACTUAL": toolchainInfo["DTXcode"],
                        "SDK_PRODUCT_BUILD_VERSION": toolchainInfo["DTPlatformBuild"],
                        "GCC_VERSION": platformInfo["DTCompiler"],
                        "XCODE_PRODUCT_BUILD_VERSION": platformInfo["DTPlatformBuild"],
                        "PLATFORM_PRODUCT_BUILD_VERSION": platformInfo["ProductBuildVersion"],
                    }
                    env["MAC_OS_X_PRODUCT_BUILD_VERSION"] = osBuildVersion;

                    for (key in extraEnv)
                        env[key] = extraEnv[key];

                    for (key in buildEnv)
                        env[key] = buildEnv[key];

                    for (key in qmakeEnv)
                        env[key] = qmakeEnv[key];

                    var expander = new DarwinTools.PropertyListVariableExpander();
                    expander.undefinedVariableFunction = function (key, varName) {
                        var msg = "Info.plist variable expansion encountered undefined variable '"
                                + varName + "' when expanding value for key '" + key
                                + "', defined in one of the following files:\n\t";
                        var allFilePaths = [];

                        for (i = 0; i < (inputs.infoplist || []).length; ++i)
                            allFilePaths.push(inputs.infoplist[i].filePath);

                        if (platformInfoPlist)
                            allFilePaths.push(platformInfoPlist);
                        msg += allFilePaths.join("\n\t") + "\n";
                        msg += "or in the bundle.infoPlist property of product '"
                                + product.name + "'";
                        console.warn(msg);
                    };
                    aggregatePlist = expander.expand(aggregatePlist, env);

                    // Add keys from partial Info.plists from asset catalogs, XIBs, and storyboards.
                    for (var j = 0; j < (inputs.partial_infoplist || []).length; ++j) {
                        var partialInfoPlist =
                                BundleTools.infoPlistContents(
                                    inputs.partial_infoplist[j].filePath)
                                || {};
                        for (key in partialInfoPlist) {
                            if (partialInfoPlist.hasOwnProperty(key)
                                    && aggregatePlist[key] === undefined)
                                aggregatePlist[key] = partialInfoPlist[key];
                        }
                    }

                }

                // Anything with an undefined or otherwise empty value should be removed
                // Only JSON-formatted plists can have null values, other formats error out
                // This also follows Xcode behavior
                DarwinTools.cleanPropertyList(aggregatePlist);

                if (infoPlistFormat === "same-as-input")
                    infoPlistFormat = "xml1";

                var validFormats = [ "xml1", "binary1", "json" ];
                if (!validFormats.contains(infoPlistFormat))
                    throw("Invalid Info.plist format " + infoPlistFormat + ". " +
                          "Must be in [xml1, binary1, json].");

                // Write the plist contents in the format appropriate for the current platform
                plist = new PropertyList();
                try {
                    plist.readFromObject(aggregatePlist);
                    plist.writeToFile(outputs.aggregate_infoplist[0].filePath, infoPlistFormat);
                } finally {
                    plist.clear();
                }
            }
            return cmd;
        }
    }

    Rule {
        condition: qbs.targetOS.contains("darwin")
        multiplex: true
        inputs: ["aggregate_infoplist"]

        outputFileTags: ["bundle.input", "pkginfo"]
        outputArtifacts: {
            var artifacts = [];
            if (ModUtils.moduleProperty(product, "isBundle") && ModUtils.moduleProperty(product, "generatePackageInfo")) {
                artifacts.push({
                    filePath: FileInfo.joinPaths(product.destinationDirectory, "PkgInfo"),
                    fileTags: ["bundle.input", "pkginfo"],
                    bundle: { _bundleFilePath: FileInfo.joinPaths(product.destinationDirectory, ModUtils.moduleProperty(product, "pkgInfoPath")) }
                });
            }
            return artifacts;
        }

        prepare: {
            var cmd = new JavaScriptCommand();
            cmd.description = "generating PkgInfo for " + product.name;
            cmd.highlight = "codegen";
            cmd.sourceCode = function() {
                var infoPlist = BundleTools.infoPlistContents(inputs.aggregate_infoplist[0].filePath);

                var pkgType = infoPlist['CFBundlePackageType'];
                if (!pkgType)
                    throw("CFBundlePackageType not found in Info.plist; this should not happen");

                var pkgSign = infoPlist['CFBundleSignature'];
                if (!pkgSign)
                    throw("CFBundleSignature not found in Info.plist; this should not happen");

                var pkginfo = new TextFile(outputs.pkginfo[0].filePath, TextFile.WriteOnly);
                pkginfo.write(pkgType + pkgSign);
                pkginfo.close();
            }
            return cmd;
        }
    }

    Rule {
        condition: qbs.targetOS.contains("darwin")
        multiplex: true
        inputs: ["bundle.input",
                 "aggregate_infoplist", "pkginfo", "hpp",
                 "icns", "xcent",
                 "compiled_ibdoc", "compiled_assetcatalog",
                 "xcode.provisioningprofile.main"]

        // Make sure the inputs of this rule are only those rules which produce outputs compatible
        // with the type of the bundle being produced.
        excludedInputs: Bundle.excludedAuxiliaryInputs(project, product)

        outputFileTags: [
            "bundle.content",
            "bundle.symlink.headers", "bundle.symlink.private-headers",
            "bundle.symlink.resources", "bundle.symlink.executable",
            "bundle.symlink.version", "bundle.hpp", "bundle.resource",
            "bundle.provisioningprofile", "bundle.content.copied", "bundle.application-executable",
            "bundle.code-signature"]
        outputArtifacts: {
            var i, artifacts = [];
            if (ModUtils.moduleProperty(product, "isBundle")) {
                for (i in inputs["bundle.input"]) {
                    var fp = inputs["bundle.input"][i].moduleProperty("bundle", "_bundleFilePath");
                    if (!fp)
                        throw("Artifact " + inputs["bundle.input"][i].filePath + " has no associated bundle file path");
                    var extraTags = inputs["bundle.input"][i].fileTags.contains("application")
                            ? ["bundle.application-executable"] : [];
                    artifacts.push({
                        filePath: fp,
                        fileTags: ["bundle.content", "bundle.content.copied"].concat(extraTags)
                    });
                }

                for (i in inputs["xcode.provisioningprofile.main"]) {
                    var ext = inputs["xcode.provisioningprofile.main"][i].fileName.split('.')[1];
                    artifacts.push({
                        filePath: FileInfo.joinPaths(product.destinationDirectory,
                                                     ModUtils.moduleProperty(product,
                                                                             "contentsFolderPath"),
                                                     "embedded." + ext),
                        fileTags: ["bundle.provisioningprofile", "bundle.content"]
                    });
                }

                var packageType = ModUtils.moduleProperty(product, "packageType");
                var isShallow = ModUtils.moduleProperty(product, "isShallow");
                if (packageType === "FMWK" && !isShallow) {
                    var publicHeaders = ModUtils.moduleProperty(product, "publicHeaders");
                    if (publicHeaders && publicHeaders.length) {
                        artifacts.push({
                            filePath: FileInfo.joinPaths(product.destinationDirectory, ModUtils.moduleProperty(product, "bundleName"), "Headers"),
                            fileTags: ["bundle.symlink.headers", "bundle.content"]
                        });
                    }

                    var privateHeaders = ModUtils.moduleProperty(product, "privateHeaders");
                    if (privateHeaders && privateHeaders.length) {
                        artifacts.push({
                            filePath: FileInfo.joinPaths(product.destinationDirectory, ModUtils.moduleProperty(product, "bundleName"), "PrivateHeaders"),
                            fileTags: ["bundle.symlink.private-headers", "bundle.content"]
                        });
                    }

                    artifacts.push({
                        filePath: FileInfo.joinPaths(product.destinationDirectory, ModUtils.moduleProperty(product, "bundleName"), "Resources"),
                        fileTags: ["bundle.symlink.resources", "bundle.content"]
                    });

                    artifacts.push({
                        filePath: FileInfo.joinPaths(product.destinationDirectory, ModUtils.moduleProperty(product, "bundleName"), product.targetName),
                        fileTags: ["bundle.symlink.executable", "bundle.content"]
                    });

                    artifacts.push({
                        filePath: FileInfo.joinPaths(product.destinationDirectory, ModUtils.moduleProperty(product, "versionsFolderPath"), "Current"),
                        fileTags: ["bundle.symlink.version", "bundle.content"]
                    });
                }

                var headerTypes = ["public", "private"];
                for (var h in headerTypes) {
                    var sources = ModUtils.moduleProperty(product, headerTypes[h] + "Headers");
                    var destination = FileInfo.joinPaths(product.destinationDirectory, ModUtils.moduleProperty(product, headerTypes[h] + "HeadersFolderPath"));
                    for (i in sources) {
                        artifacts.push({
                            filePath: FileInfo.joinPaths(destination, FileInfo.fileName(sources[i])),
                            fileTags: ["bundle.hpp", "bundle.content"]
                        });
                    }
                }

                sources = ModUtils.moduleProperty(product, "resources");
                for (i in sources) {
                    destination = BundleTools.destinationDirectoryForResource(product, {baseDir: FileInfo.path(sources[i]), fileName: FileInfo.fileName(sources[i])});
                    artifacts.push({
                        filePath: FileInfo.joinPaths(destination, FileInfo.fileName(sources[i])),
                        fileTags: ["bundle.resource", "bundle.content"]
                    });
                }

                var wrapperPath = FileInfo.joinPaths(
                            product.destinationDirectory,
                            ModUtils.moduleProperty(product, "bundleName"));
                for (var i = 0; i < artifacts.length; ++i)
                    artifacts[i].bundle = { wrapperPath: wrapperPath };

                if (product.qbs.hostOS.contains("darwin") && product.xcode
                        && product.xcode.signingIdentity) {
                    artifacts.push({
                        filePath: FileInfo.joinPaths(product.bundle.contentsFolderPath, "_CodeSignature/CodeResources"),
                        fileTags: ["bundle.code-signature", "bundle.content"]
                    });
                }
            }
            return artifacts;
        }

        prepare: {
            var i, cmd, commands = [];
            var packageType = ModUtils.moduleProperty(product, "packageType");

            var bundleType = "bundle";
            if (packageType === "APPL")
                bundleType = "application";
            if (packageType === "FMWK")
                bundleType = "framework";

            // Product is unbundled
            if (!product.bundle.isBundle) {
                cmd = new JavaScriptCommand();
                cmd.silent = true;
                cmd.sourceCode = function () { };
                commands.push(cmd);
            }

            var symlinks = outputs["bundle.symlink.version"];
            for (i in symlinks) {
                cmd = new Command("ln", ["-sfn", ModUtils.moduleProperty(product, "frameworkVersion"),
                                  symlinks[i].filePath]);
                cmd.silent = true;
                commands.push(cmd);
            }

            var publicHeaders = outputs["bundle.symlink.headers"];
            for (i in publicHeaders) {
                cmd = new Command("ln", ["-sfn", "Versions/Current/Headers",
                                         publicHeaders[i].filePath]);
                cmd.silent = true;
                commands.push(cmd);
            }

            var privateHeaders = outputs["bundle.symlink.private-headers"];
            for (i in privateHeaders) {
                cmd = new Command("ln", ["-sfn", "Versions/Current/PrivateHeaders",
                                         privateHeaders[i].filePath]);
                cmd.silent = true;
                commands.push(cmd);
            }

            var resources = outputs["bundle.symlink.resources"];
            for (i in resources) {
                cmd = new Command("ln", ["-sfn", "Versions/Current/Resources",
                                         resources[i].filePath]);
                cmd.silent = true;
                commands.push(cmd);
            }

            var executables = outputs["bundle.symlink.executable"];
            for (i in executables) {
                cmd = new Command("ln", ["-sfn", FileInfo.joinPaths("Versions", "Current", product.targetName),
                                         executables[i].filePath]);
                cmd.silent = true;
                commands.push(cmd);
            }

            function sortedArtifactList(list, func) {
                if (list) {
                    return list.sort(func || (function (a, b) {
                        return a.filePath.localeCompare(b.filePath);
                    }));
                }
            }

            var bundleInputs = sortedArtifactList(inputs["bundle.input"], function (a, b) {
                return a.moduleProperty("bundle", "_bundleFilePath").localeCompare(
                            b.moduleProperty("bundle", "_bundleFilePath"));
            });
            var bundleContents = sortedArtifactList(outputs["bundle.content.copied"]);
            for (i in bundleContents) {
                cmd = new JavaScriptCommand();
                cmd.silent = true;
                cmd.source = bundleInputs[i].filePath;
                cmd.destination = bundleContents[i].filePath;
                cmd.sourceCode = function() {
                    File.copy(source, destination);
                };
                commands.push(cmd);
            }

            var provisioningProfiles = outputs["bundle.provisioningprofile"];
            for (i in provisioningProfiles) {
                cmd = new JavaScriptCommand();
                cmd.description = "copying provisioning profile";
                cmd.highlight = "filegen";
                cmd.source = inputs["xcode.provisioningprofile.main"][i].filePath;
                cmd.destination = provisioningProfiles[i].filePath;
                cmd.sourceCode = function() {
                    File.copy(source, destination);
                };
                commands.push(cmd);
            }

            cmd = new JavaScriptCommand();
            cmd.description = "copying public headers";
            cmd.highlight = "filegen";
            cmd.sources = ModUtils.moduleProperty(product, "publicHeaders");
            cmd.destination = FileInfo.joinPaths(product.destinationDirectory, ModUtils.moduleProperty(product, "publicHeadersFolderPath"));
            cmd.sourceCode = function() {
                var i;
                for (var i in sources) {
                    File.copy(sources[i], FileInfo.joinPaths(destination, FileInfo.fileName(sources[i])));
                }
            };
            if (cmd.sources && cmd.sources.length)
                commands.push(cmd);

            cmd = new JavaScriptCommand();
            cmd.description = "copying private headers";
            cmd.highlight = "filegen";
            cmd.sources = ModUtils.moduleProperty(product, "privateHeaders");
            cmd.destination = FileInfo.joinPaths(product.destinationDirectory, ModUtils.moduleProperty(product, "privateHeadersFolderPath"));
            cmd.sourceCode = function() {
                var i;
                for (var i in sources) {
                    File.copy(sources[i], FileInfo.joinPaths(destination, FileInfo.fileName(sources[i])));
                }
            };
            if (cmd.sources && cmd.sources.length)
                commands.push(cmd);

            cmd = new JavaScriptCommand();
            cmd.description = "copying resources";
            cmd.highlight = "filegen";
            cmd.sources = ModUtils.moduleProperty(product, "resources");
            cmd.sourceCode = function() {
                var i;
                for (var i in sources) {
                    var destination = BundleTools.destinationDirectoryForResource(product, {baseDir: FileInfo.path(sources[i]), fileName: FileInfo.fileName(sources[i])});
                    File.copy(sources[i], FileInfo.joinPaths(destination, FileInfo.fileName(sources[i])));
                }
            };
            if (cmd.sources && cmd.sources.length)
                commands.push(cmd);

            if (product.moduleProperty("qbs", "hostOS").contains("darwin")) {
                var actualSigningIdentity = product.moduleProperty("xcode", "actualSigningIdentity");
                var codesignDisplayName = product.moduleProperty("xcode", "actualSigningIdentityDisplayName");
                if (actualSigningIdentity) {
                    var args = product.moduleProperty("xcode", "codesignFlags") || [];
                    args.push("--force");
                    args.push("--sign", actualSigningIdentity);
                    args = args.concat(DarwinTools._codeSignTimestampFlags(product));

                    for (var j in inputs.xcent) {
                        args.push("--entitlements", inputs.xcent[j].filePath);
                        break; // there should only be one
                    }

                    // If this is a framework, we need to sign its versioned directory
                    if (bundleType === "framework") {
                        args.push(product.bundle.contentsFolderPath);
                    } else {
                        args.push(product.bundle.bundleName);
                    }

                    cmd = new Command(product.moduleProperty("xcode", "codesignPath"), args);
                    cmd.workingDirectory = product.destinationDirectory;
                    cmd.description = "codesign "
                            + ModUtils.moduleProperty(product, "bundleName")
                            + " using " + codesignDisplayName
                            + " (" + actualSigningIdentity + ")";
                    commands.push(cmd);
                }

                if (bundleType === "application"
                        && product.moduleProperty("qbs", "targetOS").contains("macos")) {
                    cmd = new Command(ModUtils.moduleProperty(product, "lsregisterPath"),
                                      ["-f", product.bundle.bundleName]);
                    cmd.description = "register " + ModUtils.moduleProperty(product, "bundleName");
                    commands.push(cmd);
                }
            }

            return commands;
        }
    }
}
