def _process_configuration(sg_connection, config_uri_str):
    """
    Resolve and download the given Toolkit configuration.

    :param sg_connection: Shotgun connection.
    :param config_uri_str: Toolkit config descriptor as a string.
    :returns: Resolved config descriptor object.
    :raises: ValueError for "baked" descriptors.
    """
    logger.info("Analyzing configuration")

    config_uri_dict = descriptor_uri_to_dict(config_uri_str)
    if config_uri_dict["type"] == bootstrap_constants.BAKED_DESCRIPTOR_TYPE:
        raise ValueError("The given config is already baked")

    # If the config descriptor does not contain a version number, get the
    # latest.
    using_latest_config = is_descriptor_version_missing(config_uri_dict)
    if using_latest_config:
        logger.info(
            "Your configuration definition does not contain a version number. "
            "Retrieving the latest version..."
        )
    cfg_descriptor = create_descriptor(
        sg_connection,
        Descriptor.CONFIG,
        config_uri_dict,
        resolve_latest=using_latest_config,
    )
    cfg_descriptor.ensure_local()
    logger.info("Resolved config %r" % cfg_descriptor)
    return cfg_descriptor
Example #2
0
def _bake_configuration(sg_connection, manifest_data):
    """
    Bake the given configuration by ensuring it is locally cached and by modifying
    the manifest_data.

    :param sg_connection: Shotgun connection
    :param manifest_data: Manifest data as a dictionary
    :returns: The baked descriptor dictionary issued from configuration descriptor.
    """
    logger.info(
        "Baking your configuration definition into an immutable state. "
        "This means that the plugin will be frozen and no automatic updates "
        "will be performed at startup."
    )
    base_config_def = manifest_data["base_configuration"]
    if isinstance(base_config_def, str):
        # convert to dict so we can introspect
        base_config_uri_dict = descriptor_uri_to_dict(base_config_def)
    else:
        base_config_uri_dict = base_config_def

    using_latest_config = is_descriptor_version_missing(base_config_uri_dict)
    if using_latest_config:
        logger.info(
            "Your configuration definition does not contain a version number. "
            "Retrieving the latest version of the configuration for baking."
        )

    cfg_descriptor = create_descriptor(
        sg_connection,
        Descriptor.CONFIG,
        base_config_uri_dict,
        resolve_latest=using_latest_config
    )
    cfg_descriptor.ensure_local()
    local_path = cfg_descriptor.get_path()
    if not local_path:
        raise ValueError("Unable to get a local copy of %s" % cfg_descriptor)
    baked_descriptor = {
        "type": bootstrap_constants.BAKED_DESCRIPTOR_TYPE,
        "path": local_path,
        "name": cfg_descriptor.system_name,
        "version": cfg_descriptor.version
    }
    manifest_data["base_configuration"] = baked_descriptor
    return baked_descriptor
Example #3
0
def _bake_configuration(sg_connection, manifest_data):
    """
    Bake the given configuration by ensuring it is locally cached and by modifying
    the manifest_data.

    :param sg_connection: Shotgun connection
    :param manifest_data: Manifest data as a dictionary
    :returns: The baked descriptor dictionary issued from configuration descriptor.
    """
    logger.info(
        "Baking your configuration definition into an immutable state. "
        "This means that the plugin will be frozen and no automatic updates "
        "will be performed at startup.")
    base_config_def = manifest_data["base_configuration"]
    if isinstance(base_config_def, str):
        # convert to dict so we can introspect
        base_config_uri_dict = descriptor_uri_to_dict(base_config_def)
    else:
        base_config_uri_dict = base_config_def

    using_latest_config = is_descriptor_version_missing(base_config_uri_dict)
    if using_latest_config:
        logger.info(
            "Your configuration definition does not contain a version number. "
            "Retrieving the latest version of the configuration for baking.")

    cfg_descriptor = create_descriptor(
        sg_connection,
        Descriptor.CONFIG,
        base_config_uri_dict,
        resolve_latest=using_latest_config,
    )
    cfg_descriptor.ensure_local()
    local_path = cfg_descriptor.get_path()
    if not local_path:
        raise ValueError("Unable to get a local copy of %s" % cfg_descriptor)
    baked_descriptor = {
        "type": bootstrap_constants.BAKED_DESCRIPTOR_TYPE,
        "path": local_path,
        "name": cfg_descriptor.system_name,
        "version": cfg_descriptor.version,
    }
    manifest_data["base_configuration"] = baked_descriptor
    return baked_descriptor
Example #4
0
def _process_configuration(sg_connection, source_path, target_path,
                           bundle_cache_root, manifest_data):
    """
    Given data in the plugin manifest, download resolve and
    cache the configuration.

    :param sg_connection: Shotgun connection
    :param source_path: Root path of plugin source.
    :param target_path: Build target path
    :param bundle_cache_root: Bundle cache root
    :param manifest_data: Manifest data as a dictionary
    :return: (Resolved config descriptor object, config descriptor uri to use at runtime)
    """
    logger.info("Analyzing configuration")

    # get config def from info yml and generate both
    # dict and string uris.
    base_config_def = manifest_data["base_configuration"]
    if isinstance(base_config_def, str):
        # convert to dict so we can introspect
        base_config_uri_dict = descriptor_uri_to_dict(base_config_def)
        base_config_uri_str = base_config_def
    else:
        base_config_uri_dict = base_config_def
        base_config_uri_str = descriptor_dict_to_uri(base_config_def)

    # Special case - check for the 'baked' descriptor type
    # and process it. A baked descriptor is a special concept
    # that only exists in the build script. The baked descriptor
    # takes a single path parameter which can be a local or absolute
    # path. The path is copied across by the build script into a
    # manual descriptor, with a version number based on the current date.
    # This ensures that the manual descriptor will be correctly
    # re-cached at bootstrap time.
    if base_config_uri_dict[
            "type"] == bootstrap_constants.BAKED_DESCRIPTOR_TYPE:
        logger.info("Baked descriptor detected.")

        baked_path = os.path.expanduser(
            os.path.expandvars(base_config_uri_dict["path"]))

        # if it's a relative path, expand it
        if not os.path.isabs(baked_path):
            full_baked_path = os.path.abspath(
                os.path.join(source_path, baked_path))

            # if it's a relative path, we have already copied it to the build
            # target location. In this case, attempt to locate it and remove it.
            baked_target_path = os.path.abspath(
                os.path.join(target_path, baked_path))
            if baked_target_path.startswith(baked_target_path):
                logger.debug("Removing '%s' from build" % baked_target_path)
                shutil.rmtree(baked_target_path)
        else:
            # path is absolute
            full_baked_path = os.path.abspath(baked_path)

        logger.info("Will bake an immutable config into the plugin from '%s'" %
                    full_baked_path)

        install_path = os.path.join(
            bundle_cache_root,
            bootstrap_constants.BAKED_DESCRIPTOR_FOLDER_NAME,
            BAKED_BUNDLE_NAME, BAKED_BUNDLE_VERSION)

        cfg_descriptor = create_descriptor(sg_connection, Descriptor.CONFIG, {
            "type": "path",
            "path": full_baked_path
        })

        BakedConfiguration.bake_config_scaffold(install_path, sg_connection,
                                                manifest_data["plugin_id"],
                                                cfg_descriptor)

        # now lastly,
        base_config_uri_str = descriptor_dict_to_uri({
            "type":
            bootstrap_constants.BAKED_DESCRIPTOR_TYPE,
            "name":
            BAKED_BUNDLE_NAME,
            "version":
            BAKED_BUNDLE_VERSION
        })

    else:

        # if the descriptor in the config contains a version number
        # we will go into a fixed update mode.
        if is_descriptor_version_missing(base_config_uri_dict):
            logger.info(
                "Your configuration definition does not contain a version number. "
                "This means that the plugin will attempt to auto update at startup."
            )
            using_latest_config = True
        else:
            logger.info(
                "Your configuration definition contains a version number. "
                "This means that the plugin will be frozen and no automatic updates "
                "will be performed at startup.")
            using_latest_config = False

        cfg_descriptor = create_descriptor(sg_connection,
                                           Descriptor.CONFIG,
                                           base_config_uri_dict,
                                           resolve_latest=using_latest_config)

    logger.info("Resolved config %r" % cfg_descriptor)
    logger.info("Runtime config descriptor uri will be %s" %
                base_config_uri_str)
    return cfg_descriptor, base_config_uri_str
Example #5
0
def build_plugin(sg_connection,
                 source_path,
                 target_path,
                 bootstrap_core_uri=None):
    """
    Perform a build of a plugin.

    This will introspect the info.yml in the source path,
    copy over everything in the source path into target path
    and then establish a bundle cache containing a reflection
    of all items required by the config.

    :param sg_connection: Shotgun connection
    :param source_path: Path to plugin.
    :param target_path: Path to build
    :param bootstrap_core_uri: Custom bootstrap core uri. If None,
                               the latest core from the app store will be used.
    """
    logger.info("Your toolkit plugin in '%s' will be processed." % source_path)
    logger.info("The build will generated into '%s'" % target_path)

    # check for existence
    if not os.path.exists(source_path):
        raise TankError("Source path '%s' cannot be found on disk!" %
                        source_path)

    # check that target path doesn't exist
    if os.path.exists(target_path):
        logger.info("The folder '%s' already exists on disk. Removing it" %
                    target_path)
        wipe_folder(target_path)

    # try to create target path
    filesystem.ensure_folder_exists(target_path)

    # check manifest
    manifest_data = _validate_manifest(source_path)

    # copy all plugin data across
    # skip info.yml, this is baked into the manifest python code
    logger.info("Copying plugin data across...")
    filesystem.copy_folder(source_path, target_path)

    # create bundle cache
    logger.info("Creating bundle cache folder...")
    bundle_cache_root = os.path.join(target_path,
                                     BUNDLE_CACHE_ROOT_FOLDER_NAME)
    filesystem.ensure_folder_exists(bundle_cache_root)

    # resolve config descriptor
    # the config_uri_str returned by the method contains the fully resolved
    # uri to use at runtime - in the case of baked descriptors, the config_uri_str
    # contains a manual descriptor uri.
    (cfg_descriptor,
     config_uri_str) = _process_configuration(sg_connection, source_path,
                                              target_path, bundle_cache_root,
                                              manifest_data)

    # cache config in bundle cache
    logger.info("Downloading and caching config...")

    # copy the config payload across to the plugin bundle cache
    cfg_descriptor.clone_cache(bundle_cache_root)

    # cache all apps, engines and frameworks
    cache_apps(sg_connection, cfg_descriptor, bundle_cache_root)

    # get latest core - cache it directly into the plugin root folder
    if bootstrap_core_uri:
        logger.info("Caching custom core for boostrap (%s)" %
                    bootstrap_core_uri)
        bootstrap_core_desc = create_descriptor(
            sg_connection,
            Descriptor.CORE,
            bootstrap_core_uri,
            resolve_latest=is_descriptor_version_missing(bootstrap_core_uri),
            bundle_cache_root_override=bundle_cache_root)

    else:
        # by default, use latest core for bootstrap
        logger.info(
            "Caching latest official core to use when bootstrapping plugin.")
        logger.info(
            "(To use a specific config instead, specify a --bootstrap-core-uri flag.)"
        )

        bootstrap_core_desc = create_descriptor(
            sg_connection,
            Descriptor.CORE, {
                "type": "app_store",
                "name": "tk-core"
            },
            resolve_latest=True,
            bundle_cache_root_override=bundle_cache_root)

    # cache it
    bootstrap_core_desc.ensure_local()

    # make a python folder where we put our manifest
    logger.info("Creating configuration manifest...")

    # bake out the manifest into python files.
    _bake_manifest(manifest_data, config_uri_str, bootstrap_core_desc,
                   target_path)

    # now analyze what core the config needs
    if cfg_descriptor.associated_core_descriptor:
        logger.info(
            "Config is specifying a custom core in config/core/core_api.yml.")
        logger.info("This will be used when the config is executing.")
        logger.info("Ensuring this core (%s) is cached..." %
                    cfg_descriptor.associated_core_descriptor)
        associated_core_desc = create_descriptor(
            sg_connection,
            Descriptor.CORE,
            cfg_descriptor.associated_core_descriptor,
            bundle_cache_root_override=bundle_cache_root)
        associated_core_desc.ensure_local()

    cleanup_bundle_cache(bundle_cache_root)

    logger.info("")
    logger.info("Build complete!")
    logger.info("")
    logger.info("- Your plugin is ready in '%s'" % target_path)
    logger.info("- Plugin uses config %r" % cfg_descriptor)
    logger.info("- Bootstrap core is %r" % bootstrap_core_desc)
    logger.info(
        "- All dependencies have been baked out into the bundle_cache folder")
    logger.info("")
    logger.info("")
    logger.info("")
def build_plugin(sg_connection,
                 source_path,
                 target_path,
                 bootstrap_core_uri=None,
                 do_bake=False,
                 use_system_core=False):
    """
    Perform a build of a plugin.

    This will introspect the info.yml in the source path,
    copy over everything in the source path into target path
    and then establish a bundle cache containing a reflection
    of all items required by the config.

    :param sg_connection: Shotgun connection
    :param source_path: Path to plugin.
    :param target_path: Path to build
    :param bootstrap_core_uri: Custom bootstrap core uri. If None,
                               the latest core from the app store will be used.
    :param bool do_bake: If True, bake the plugin prior to building it.
    :param bool use_system_core: If True, use a globally installed tk-core instead
                                 of the one specified in the configuration.
    """
    logger.info("Your toolkit plugin in '%s' will be processed." % source_path)
    logger.info("The build will %s into '%s'" %
                (["generated", "baked"][do_bake], target_path))

    # check for existence
    if not os.path.exists(source_path):
        raise TankError("Source path '%s' cannot be found on disk!" %
                        source_path)

    # check manifest
    manifest_data = _validate_manifest(source_path)

    if do_bake:
        baked_descriptor = _bake_configuration(
            sg_connection,
            manifest_data,
        )
        # When baking we control the output path by adding a folder based on the
        # configuration descriptor and version.
        target_path = os.path.join(
            target_path,
            "%s-%s" % (baked_descriptor["name"], baked_descriptor["version"]))

    # check that target path doesn't exist
    if os.path.exists(target_path):
        logger.info("The folder '%s' already exists on disk. Removing it" %
                    target_path)
        wipe_folder(target_path)

    # try to create target path
    filesystem.ensure_folder_exists(target_path)

    # copy all plugin data across
    # skip info.yml, this is baked into the manifest python code
    logger.info("Copying plugin data across...")
    filesystem.copy_folder(source_path, target_path)

    # create bundle cache
    logger.info("Creating bundle cache folder...")
    bundle_cache_root = os.path.join(target_path,
                                     BUNDLE_CACHE_ROOT_FOLDER_NAME)
    filesystem.ensure_folder_exists(bundle_cache_root)

    # resolve config descriptor
    # the config_uri_str returned by the method contains the fully resolved
    # uri to use at runtime - in the case of baked descriptors, the config_uri_str
    # contains a manual descriptor uri and install_path is set with the baked
    # folder.
    (cfg_descriptor, config_uri_str,
     install_path) = _process_configuration(sg_connection, source_path,
                                            target_path, bundle_cache_root,
                                            manifest_data, use_system_core)

    # cache config in bundle cache
    logger.info("Downloading and caching config...")

    # copy the config payload across to the plugin bundle cache
    cfg_descriptor.clone_cache(bundle_cache_root)

    # cache all apps, engines and frameworks
    cache_apps(sg_connection, cfg_descriptor, bundle_cache_root)

    if use_system_core:
        logger.info(
            "An external core will be used for this plugin, not caching it")
        bootstrap_core_desc = None
    else:
        # get core - cache it directly into the plugin root folder
        if bootstrap_core_uri:
            logger.info("Caching custom core for boostrap (%s)" %
                        bootstrap_core_uri)
            bootstrap_core_desc = create_descriptor(
                sg_connection,
                Descriptor.CORE,
                bootstrap_core_uri,
                resolve_latest=is_descriptor_version_missing(
                    bootstrap_core_uri),
                bundle_cache_root_override=bundle_cache_root)
            # cache it
            bootstrap_core_desc.ensure_local()

        elif not cfg_descriptor.associated_core_descriptor:
            # by default, use latest core for bootstrap
            logger.info(
                "Caching latest official core to use when bootstrapping plugin."
            )
            logger.info(
                "(To use a specific config instead, specify a --bootstrap-core-uri flag.)"
            )

            bootstrap_core_desc = create_descriptor(
                sg_connection,
                Descriptor.CORE, {
                    "type": "app_store",
                    "name": "tk-core"
                },
                resolve_latest=True,
                bundle_cache_root_override=bundle_cache_root)

            # cache it
            bootstrap_core_desc.ensure_local()
        else:
            # The bootstrap core will be derived from the associated core desc below.
            bootstrap_core_desc = None

    # now analyze what core the config needs
    if not use_system_core and cfg_descriptor.associated_core_descriptor:
        logger.info(
            "Config is specifying a custom core in config/core/core_api.yml.")
        logger.info("This will be used when the config is executing.")
        logger.info("Ensuring this core (%s) is cached..." %
                    cfg_descriptor.associated_core_descriptor)
        associated_core_desc = create_descriptor(
            sg_connection,
            Descriptor.CORE,
            cfg_descriptor.associated_core_descriptor,
            bundle_cache_root_override=bundle_cache_root)
        associated_core_desc.ensure_local()
        if bootstrap_core_desc is None:
            # Use the same version as the one specified by the config.
            if install_path:
                # Install path is set only if the config was baked. We re-use the
                # install path as an optimisation to avoid core swapping when the
                # config is bootstrapped.
                logger.info(
                    "Bootstrapping will use installed %s required by the config"
                    % associated_core_desc)
                # If the core was installed we directly use it.
                bootstrap_core_desc = create_descriptor(
                    sg_connection,
                    Descriptor.CORE, {
                        "type": "path",
                        "name": "tk-core",
                        "path": os.path.join(install_path, "install", "core"),
                        "version": associated_core_desc.version,
                    },
                    resolve_latest=False,
                    bundle_cache_root_override=bundle_cache_root)
            else:
                logger.info(
                    "Bootstrapping will use core %s required by the config" %
                    associated_core_desc)
                bootstrap_core_desc = associated_core_desc

    # make a python folder where we put our manifest
    logger.info("Creating configuration manifest...")

    # bake out the manifest into python files.
    _bake_manifest(manifest_data, config_uri_str, bootstrap_core_desc,
                   target_path)

    cleanup_bundle_cache(bundle_cache_root)

    logger.info("")
    logger.info("Build complete!")
    logger.info("")
    logger.info("- Your plugin is ready in '%s'" % target_path)
    logger.info("- Plugin uses config %r" % cfg_descriptor)
    if bootstrap_core_desc:
        logger.info("- Bootstrap core is %r" % bootstrap_core_desc)
    else:
        logger.info("- Plugin will need an external installed core.")
    logger.info(
        "- All dependencies have been baked out into the bundle_cache folder")
    logger.info("")
    logger.info("")
    logger.info("")
def _process_configuration(sg_connection, source_path, target_path,
                           bundle_cache_root, manifest_data, use_system_core):
    """
    Given data in the plugin manifest, download resolve and
    cache the configuration.

    :param sg_connection: Shotgun connection
    :param source_path: Root path of plugin source.
    :param target_path: Build target path
    :param bundle_cache_root: Bundle cache root
    :param manifest_data: Manifest data as a dictionary
    :param bool use_system_core: If True, use a globally installed tk-core instead
                                 of the one specified in the configuration.
    :return: (Resolved config descriptor object, config descriptor uri to use at runtime, install path)
    """
    logger.info("Analyzing configuration")

    install_path = None

    # get config def from info yml and generate both
    # dict and string uris.
    base_config_def = manifest_data["base_configuration"]
    if isinstance(base_config_def, str):
        # convert to dict so we can introspect
        base_config_uri_dict = descriptor_uri_to_dict(base_config_def)
        base_config_uri_str = base_config_def
    else:
        base_config_uri_dict = base_config_def
        base_config_uri_str = descriptor_dict_to_uri(base_config_def)

    # Special case - check for the 'baked' descriptor type
    # and process it. A baked descriptor is a special concept
    # that only exists in the build script. The baked descriptor
    # takes a single path parameter which can be a local or absolute
    # path. The path is copied across by the build script into a
    # manual descriptor, with a version number based on the current date.
    # This ensures that the manual descriptor will be correctly
    # re-cached at bootstrap time.
    if base_config_uri_dict[
            "type"] == bootstrap_constants.BAKED_DESCRIPTOR_TYPE:
        logger.info("Baked descriptor detected.")

        baked_path = os.path.expanduser(
            os.path.expandvars(base_config_uri_dict["path"]))

        # if it's a relative path, expand it
        if not os.path.isabs(baked_path):
            full_baked_path = os.path.abspath(
                os.path.join(source_path, baked_path))

            # if it's a relative path, we have already copied it to the build
            # target location. In this case, attempt to locate it and remove it.
            baked_target_path = os.path.abspath(
                os.path.join(target_path, baked_path))
            if baked_target_path.startswith(baked_target_path):
                logger.debug("Removing '%s' from build" % baked_target_path)
                shutil.rmtree(baked_target_path)
        else:
            # path is absolute
            full_baked_path = os.path.abspath(baked_path)

        logger.info("Will bake an immutable config into the plugin from '%s'" %
                    full_baked_path)

        # A baked config descriptor does not require a name nor a version, so
        # if these keys are not available, use the current date time for the
        # version and an arbitrary name for the config. Please note that this
        # only happens if the baked descriptor was set in the original config.
        # When baking a plugin with the --bake option, which is the recommended
        # workflow, these values are automatically set.
        baked_name = base_config_uri_dict.get("name") or "tk-config-plugin"
        baked_version = (base_config_uri_dict.get("version")
                         or datetime.datetime.now().strftime("%Y%m%d_%H%M%S"))
        install_path = os.path.join(
            bundle_cache_root,
            bootstrap_constants.BAKED_DESCRIPTOR_FOLDER_NAME, baked_name,
            baked_version)

        cfg_descriptor = create_descriptor(sg_connection, Descriptor.CONFIG, {
            "type": "path",
            "path": full_baked_path
        })

        BakedConfiguration.bake_config_scaffold(install_path, sg_connection,
                                                manifest_data["plugin_id"],
                                                cfg_descriptor)

        # now lastly,
        base_config_uri_str = descriptor_dict_to_uri({
            "type":
            bootstrap_constants.BAKED_DESCRIPTOR_TYPE,
            "name":
            baked_name,
            "version":
            baked_version
        })
        if use_system_core:
            # If asked to use a globally installed tk-core instead of the one
            # specified by the config, we remove the local copy which was created
            # in the scaffold step.
            logger.info("Removing core reference in %s" % install_path)
            wipe_folder(os.path.join(install_path, "install"))
            # And make sure we don't have any reference to a tk-core in the config,
            # otherwise it would be picked up when bootstrapping.
            filesystem.safe_delete_file(
                os.path.join(install_path, "config", "core", "core_api.yml"))
        else:
            # Workaround for tk-core bootstrap needing a shotgun.yml file: when swapping
            # tk-core, this file is checked to see if a script user was specified and
            # should be used in place of the authenticated user. So we create a dummy
            # file with an "unspecified" host, as the key is required by the tk-core
            # code parsing the file.
            # It is not clear if this workaround is needed for non baked configs as
            # their workflow is different, so for now we just keep it for bake configs
            # only.
            shotgun_yaml_path = os.path.join(install_path, "config", "core",
                                             "shotgun.yml")
            if not os.path.exists(shotgun_yaml_path):
                logger.info("Patching %s" % shotgun_yaml_path)
                with open(shotgun_yaml_path, "w") as pf:
                    pf.write(
                        "# Workaround for tk-core bootstrap\nhost: unspecified"
                    )
    else:

        # if the descriptor in the config contains a version number
        # we will go into a fixed update mode.
        using_latest_config = is_descriptor_version_missing(
            base_config_uri_dict)
        if using_latest_config:
            logger.info(
                "Your configuration definition does not contain a version number. "
                "This means that the plugin will attempt to auto update at startup."
            )
        else:
            logger.info(
                "Your configuration definition contains a version number. "
                "This means that the plugin will be frozen and no automatic updates "
                "will be performed at startup.")

        cfg_descriptor = create_descriptor(sg_connection,
                                           Descriptor.CONFIG,
                                           base_config_uri_dict,
                                           resolve_latest=using_latest_config)
    logger.info("Resolved config %r" % cfg_descriptor)
    logger.info("Runtime config descriptor uri will be %s" %
                base_config_uri_str)
    if install_path:
        logger.info("The config was baked in %s" % install_path)
    return cfg_descriptor, base_config_uri_str, install_path
Example #8
0
def _process_configuration(sg_connection, source_path, target_path, bundle_cache_root, manifest_data):
    """
    Given data in the plugin manifest, download resolve and
    cache the configuration.

    :param sg_connection: Shotgun connection
    :param source_path: Root path of plugin source.
    :param target_path: Build target path
    :param bundle_cache_root: Bundle cache root
    :param manifest_data: Manifest data as a dictionary
    :return: (Resolved config descriptor object, config descriptor uri to use at runtime)
    """
    logger.info("Analyzing configuration")

    # get config def from info yml and generate both
    # dict and string uris.
    base_config_def = manifest_data["base_configuration"]
    if isinstance(base_config_def, str):
        # convert to dict so we can introspect
        base_config_uri_dict = descriptor_uri_to_dict(base_config_def)
        base_config_uri_str = base_config_def
    else:
        base_config_uri_dict = base_config_def
        base_config_uri_str = descriptor_dict_to_uri(base_config_def)

    # Special case - check for the 'baked' descriptor type
    # and process it. A baked descriptor is a special concept
    # that only exists in the build script. The baked descriptor
    # takes a single path parameter which can be a local or absolute
    # path. The path is copied across by the build script into a
    # manual descriptor, with a version number based on the current date.
    # This ensures that the manual descriptor will be correctly
    # re-cached at bootstrap time.
    if base_config_uri_dict["type"] == bootstrap_constants.BAKED_DESCRIPTOR_TYPE:
        logger.info("Baked descriptor detected.")

        baked_path = os.path.expanduser(os.path.expandvars(base_config_uri_dict["path"]))

        # if it's a relative path, expand it
        if not os.path.isabs(baked_path):
            full_baked_path = os.path.abspath(os.path.join(source_path, baked_path))

            # if it's a relative path, we have already copied it to the build
            # target location. In this case, attempt to locate it and remove it.
            baked_target_path = os.path.abspath(os.path.join(target_path, baked_path))
            if baked_target_path.startswith(baked_target_path):
                logger.debug("Removing '%s' from build" % baked_target_path)
                shutil.rmtree(baked_target_path)
        else:
            # path is absolute
            full_baked_path = os.path.abspath(baked_path)

        logger.info("Will bake an immutable config into the plugin from '%s'" % full_baked_path)

        install_path = os.path.join(
            bundle_cache_root,
            bootstrap_constants.BAKED_DESCRIPTOR_FOLDER_NAME,
            BAKED_BUNDLE_NAME,
            BAKED_BUNDLE_VERSION
        )

        cfg_descriptor = create_descriptor(
            sg_connection,
            Descriptor.CONFIG,
            {"type": "path", "path": full_baked_path}
        )

        BakedConfiguration.bake_config_scaffold(
            install_path,
            sg_connection,
            manifest_data["plugin_id"],
            cfg_descriptor
        )

        # now lastly,
        base_config_uri_str = descriptor_dict_to_uri(
            {
                "type": bootstrap_constants.BAKED_DESCRIPTOR_TYPE,
                "name": BAKED_BUNDLE_NAME,
                "version": BAKED_BUNDLE_VERSION
            }
        )

    else:

        # if the descriptor in the config contains a version number
        # we will go into a fixed update mode.
        if is_descriptor_version_missing(base_config_uri_dict):
            logger.info(
                "Your configuration definition does not contain a version number. "
                "This means that the plugin will attempt to auto update at startup."
            )
            using_latest_config = True
        else:
            logger.info(
                "Your configuration definition contains a version number. "
                "This means that the plugin will be frozen and no automatic updates "
                "will be performed at startup."
            )
            using_latest_config = False

        cfg_descriptor = create_descriptor(
            sg_connection,
            Descriptor.CONFIG,
            base_config_uri_dict,
            resolve_latest=using_latest_config
        )

    logger.info("Resolved config %r" % cfg_descriptor)
    logger.info("Runtime config descriptor uri will be %s" % base_config_uri_str)
    return cfg_descriptor, base_config_uri_str
Example #9
0
def build_plugin(sg_connection, source_path, target_path, bootstrap_core_uri=None):
    """
    Perform a build of a plugin.

    This will introspect the info.yml in the source path,
    copy over everything in the source path into target path
    and then establish a bundle cache containing a reflection
    of all items required by the config.

    :param sg_connection: Shotgun connection
    :param source_path: Path to plugin.
    :param target_path: Path to build
    :param bootstrap_core_uri: Custom bootstrap core uri. If None,
                               the latest core from the app store will be used.
    """
    logger.info("Your toolkit plugin in '%s' will be processed." % source_path)
    logger.info("The build will generated into '%s'" % target_path)

    # check for existence
    if not os.path.exists(source_path):
        raise TankError("Source path '%s' cannot be found on disk!" % source_path)

    # check that target path doesn't exist
    if os.path.exists(target_path):
        logger.info("The folder '%s' already exists on disk. Removing it" % target_path)
        wipe_folder(target_path)

    # try to create target path
    filesystem.ensure_folder_exists(target_path)

    # check manifest
    manifest_data = _validate_manifest(source_path)

    # copy all plugin data across
    # skip info.yml, this is baked into the manifest python code
    logger.info("Copying plugin data across...")
    filesystem.copy_folder(source_path, target_path)

    # create bundle cache
    logger.info("Creating bundle cache folder...")
    bundle_cache_root = os.path.join(target_path, BUNDLE_CACHE_ROOT_FOLDER_NAME)
    filesystem.ensure_folder_exists(bundle_cache_root)

    # resolve config descriptor
    # the config_uri_str returned by the method contains the fully resolved
    # uri to use at runtime - in the case of baked descriptors, the config_uri_str
    # contains a manual descriptor uri.
    (cfg_descriptor, config_uri_str) = _process_configuration(
        sg_connection,
        source_path,
        target_path,
        bundle_cache_root,
        manifest_data
    )

    # cache config in bundle cache
    logger.info("Downloading and caching config...")

    # copy the config payload across to the plugin bundle cache
    cfg_descriptor.clone_cache(bundle_cache_root)

    # cache all apps, engines and frameworks
    cache_apps(sg_connection, cfg_descriptor, bundle_cache_root)

    # get latest core - cache it directly into the plugin root folder
    if bootstrap_core_uri:
        logger.info("Caching custom core for boostrap (%s)" % bootstrap_core_uri)
        bootstrap_core_desc = create_descriptor(
            sg_connection,
            Descriptor.CORE,
            bootstrap_core_uri,
            resolve_latest=is_descriptor_version_missing(bootstrap_core_uri),
            bundle_cache_root_override=bundle_cache_root
        )

    else:
        # by default, use latest core for bootstrap
        logger.info("Caching latest official core to use when bootstrapping plugin.")
        logger.info("(To use a specific config instead, specify a --bootstrap-core-uri flag.)")

        bootstrap_core_desc = create_descriptor(
            sg_connection,
            Descriptor.CORE,
            {"type": "app_store", "name": "tk-core"},
            resolve_latest=True,
            bundle_cache_root_override=bundle_cache_root
        )

    # cache it
    bootstrap_core_desc.ensure_local()

    # make a python folder where we put our manifest
    logger.info("Creating configuration manifest...")

    # bake out the manifest into python files.
    _bake_manifest(
        manifest_data,
        config_uri_str,
        bootstrap_core_desc,
        target_path
    )

    # now analyze what core the config needs
    if cfg_descriptor.associated_core_descriptor:
        logger.info("Config is specifying a custom core in config/core/core_api.yml.")
        logger.info("This will be used when the config is executing.")
        logger.info("Ensuring this core (%s) is cached..." % cfg_descriptor.associated_core_descriptor)
        associated_core_desc = create_descriptor(
            sg_connection,
            Descriptor.CORE,
            cfg_descriptor.associated_core_descriptor,
            bundle_cache_root_override=bundle_cache_root
        )
        associated_core_desc.ensure_local()

    cleanup_bundle_cache(bundle_cache_root)

    logger.info("")
    logger.info("Build complete!")
    logger.info("")
    logger.info("- Your plugin is ready in '%s'" % target_path)
    logger.info("- Plugin uses config %r" % cfg_descriptor)
    logger.info("- Bootstrap core is %r" % bootstrap_core_desc)
    logger.info("- All dependencies have been baked out into the bundle_cache folder")
    logger.info("")
    logger.info("")
    logger.info("")
Example #10
0
def build_plugin(sg_connection, source_path, target_path, bootstrap_core_uri=None, do_bake=False, use_system_core=False):
    """
    Perform a build of a plugin.

    This will introspect the info.yml in the source path,
    copy over everything in the source path into target path
    and then establish a bundle cache containing a reflection
    of all items required by the config.

    :param sg_connection: Shotgun connection
    :param source_path: Path to plugin.
    :param target_path: Path to build
    :param bootstrap_core_uri: Custom bootstrap core uri. If None,
                               the latest core from the app store will be used.
    :param bool do_bake: If True, bake the plugin prior to building it.
    :param bool use_system_core: If True, use a globally installed tk-core instead
                                 of the one specified in the configuration.
    """
    logger.info("Your toolkit plugin in '%s' will be processed." % source_path)
    logger.info("The build will %s into '%s'" % (["generated", "baked"][do_bake], target_path))

    # check for existence
    if not os.path.exists(source_path):
        raise TankError("Source path '%s' cannot be found on disk!" % source_path)

    # check manifest
    manifest_data = _validate_manifest(source_path)

    if do_bake:
        baked_descriptor = _bake_configuration(
            sg_connection,
            manifest_data,
        )
        # When baking we control the output path by adding a folder based on the
        # configuration descriptor and version.
        target_path = os.path.join(target_path, "%s-%s" % (
            baked_descriptor["name"],
            baked_descriptor["version"]
        ))

    # check that target path doesn't exist
    if os.path.exists(target_path):
        logger.info("The folder '%s' already exists on disk. Removing it" % target_path)
        wipe_folder(target_path)

    # try to create target path
    filesystem.ensure_folder_exists(target_path)

    # copy all plugin data across
    # skip info.yml, this is baked into the manifest python code
    logger.info("Copying plugin data across...")
    filesystem.copy_folder(source_path, target_path)

    # create bundle cache
    logger.info("Creating bundle cache folder...")
    bundle_cache_root = os.path.join(target_path, BUNDLE_CACHE_ROOT_FOLDER_NAME)
    filesystem.ensure_folder_exists(bundle_cache_root)

    # resolve config descriptor
    # the config_uri_str returned by the method contains the fully resolved
    # uri to use at runtime - in the case of baked descriptors, the config_uri_str
    # contains a manual descriptor uri and install_path is set with the baked
    # folder.
    (cfg_descriptor, config_uri_str, install_path) = _process_configuration(
        sg_connection,
        source_path,
        target_path,
        bundle_cache_root,
        manifest_data,
        use_system_core
    )

    # cache config in bundle cache
    logger.info("Downloading and caching config...")

    # copy the config payload across to the plugin bundle cache
    cfg_descriptor.clone_cache(bundle_cache_root)

    # cache all apps, engines and frameworks
    cache_apps(sg_connection, cfg_descriptor, bundle_cache_root)

    if use_system_core:
        logger.info("An external core will be used for this plugin, not caching it")
        bootstrap_core_desc = None
    else:
        # get core - cache it directly into the plugin root folder
        if bootstrap_core_uri:
            logger.info("Caching custom core for boostrap (%s)" % bootstrap_core_uri)
            bootstrap_core_desc = create_descriptor(
                sg_connection,
                Descriptor.CORE,
                bootstrap_core_uri,
                resolve_latest=is_descriptor_version_missing(bootstrap_core_uri),
                bundle_cache_root_override=bundle_cache_root
            )
            # cache it
            bootstrap_core_desc.ensure_local()

        elif not cfg_descriptor.associated_core_descriptor:
            # by default, use latest core for bootstrap
            logger.info("Caching latest official core to use when bootstrapping plugin.")
            logger.info("(To use a specific config instead, specify a --bootstrap-core-uri flag.)")

            bootstrap_core_desc = create_descriptor(
                sg_connection,
                Descriptor.CORE,
                {"type": "app_store", "name": "tk-core"},
                resolve_latest=True,
                bundle_cache_root_override=bundle_cache_root
            )

            # cache it
            bootstrap_core_desc.ensure_local()
        else:
            # The bootstrap core will be derived from the associated core desc below.
            bootstrap_core_desc = None

    # now analyze what core the config needs
    if not use_system_core and cfg_descriptor.associated_core_descriptor:
        logger.info("Config is specifying a custom core in config/core/core_api.yml.")
        logger.info("This will be used when the config is executing.")
        logger.info("Ensuring this core (%s) is cached..." % cfg_descriptor.associated_core_descriptor)
        associated_core_desc = create_descriptor(
            sg_connection,
            Descriptor.CORE,
            cfg_descriptor.associated_core_descriptor,
            bundle_cache_root_override=bundle_cache_root
        )
        associated_core_desc.ensure_local()
        if bootstrap_core_desc is None:
            # Use the same version as the one specified by the config.
            if install_path:
                # Install path is set only if the config was baked. We re-use the
                # install path as an optimisation to avoid core swapping when the
                # config is bootstrapped.
                logger.info(
                    "Bootstrapping will use installed %s required by the config" %
                    associated_core_desc
                )
                # If the core was installed we directly use it.
                bootstrap_core_desc = create_descriptor(
                    sg_connection,
                    Descriptor.CORE, {
                        "type": "path",
                        "name": "tk-core",
                        "path": os.path.join(install_path, "install", "core"),
                        "version": associated_core_desc.version,
                    },
                    resolve_latest=False,
                    bundle_cache_root_override=bundle_cache_root
                )
            else:
                logger.info(
                    "Bootstrapping will use core %s required by the config" %
                    associated_core_desc
                )
                bootstrap_core_desc = associated_core_desc

    # make a python folder where we put our manifest
    logger.info("Creating configuration manifest...")

    # bake out the manifest into python files.
    _bake_manifest(
        manifest_data,
        config_uri_str,
        bootstrap_core_desc,
        target_path
    )

    cleanup_bundle_cache(bundle_cache_root)

    logger.info("")
    logger.info("Build complete!")
    logger.info("")
    logger.info("- Your plugin is ready in '%s'" % target_path)
    logger.info("- Plugin uses config %r" % cfg_descriptor)
    if bootstrap_core_desc:
        logger.info("- Bootstrap core is %r" % bootstrap_core_desc)
    else:
        logger.info("- Plugin will need an external installed core.")
    logger.info("- All dependencies have been baked out into the bundle_cache folder")
    logger.info("")
    logger.info("")
    logger.info("")
Example #11
0
def _process_configuration(sg_connection, source_path, target_path, bundle_cache_root, manifest_data, use_system_core):
    """
    Given data in the plugin manifest, download resolve and
    cache the configuration.

    :param sg_connection: Shotgun connection
    :param source_path: Root path of plugin source.
    :param target_path: Build target path
    :param bundle_cache_root: Bundle cache root
    :param manifest_data: Manifest data as a dictionary
    :param bool use_system_core: If True, use a globally installed tk-core instead
                                 of the one specified in the configuration.
    :return: (Resolved config descriptor object, config descriptor uri to use at runtime, install path)
    """
    logger.info("Analyzing configuration")

    install_path = None

    # get config def from info yml and generate both
    # dict and string uris.
    base_config_def = manifest_data["base_configuration"]
    if isinstance(base_config_def, str):
        # convert to dict so we can introspect
        base_config_uri_dict = descriptor_uri_to_dict(base_config_def)
        base_config_uri_str = base_config_def
    else:
        base_config_uri_dict = base_config_def
        base_config_uri_str = descriptor_dict_to_uri(base_config_def)

    # Special case - check for the 'baked' descriptor type
    # and process it. A baked descriptor is a special concept
    # that only exists in the build script. The baked descriptor
    # takes a single path parameter which can be a local or absolute
    # path. The path is copied across by the build script into a
    # manual descriptor, with a version number based on the current date.
    # This ensures that the manual descriptor will be correctly
    # re-cached at bootstrap time.
    if base_config_uri_dict["type"] == bootstrap_constants.BAKED_DESCRIPTOR_TYPE:
        logger.info("Baked descriptor detected.")

        baked_path = os.path.expanduser(os.path.expandvars(base_config_uri_dict["path"]))

        # if it's a relative path, expand it
        if not os.path.isabs(baked_path):
            full_baked_path = os.path.abspath(os.path.join(source_path, baked_path))

            # if it's a relative path, we have already copied it to the build
            # target location. In this case, attempt to locate it and remove it.
            baked_target_path = os.path.abspath(os.path.join(target_path, baked_path))
            if baked_target_path.startswith(baked_target_path):
                logger.debug("Removing '%s' from build" % baked_target_path)
                shutil.rmtree(baked_target_path)
        else:
            # path is absolute
            full_baked_path = os.path.abspath(baked_path)

        logger.info("Will bake an immutable config into the plugin from '%s'" % full_baked_path)

        # A baked config descriptor does not require a name nor a version, so
        # if these keys are not available, use the current date time for the
        # version and an arbitrary name for the config. Please note that this
        # only happens if the baked descriptor was set in the original config.
        # When baking a plugin with the --bake option, which is the recommended
        # workflow, these values are automatically set.
        baked_name = base_config_uri_dict.get("name") or "tk-config-plugin"
        baked_version = (
            base_config_uri_dict.get("version") or
            datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
        )
        install_path = os.path.join(
            bundle_cache_root,
            bootstrap_constants.BAKED_DESCRIPTOR_FOLDER_NAME,
            baked_name,
            baked_version
        )

        cfg_descriptor = create_descriptor(
            sg_connection,
            Descriptor.CONFIG,
            {"type": "path", "path": full_baked_path}
        )

        BakedConfiguration.bake_config_scaffold(
            install_path,
            sg_connection,
            manifest_data["plugin_id"],
            cfg_descriptor
        )

        # now lastly,
        base_config_uri_str = descriptor_dict_to_uri(
            {
                "type": bootstrap_constants.BAKED_DESCRIPTOR_TYPE,
                "name": baked_name,
                "version": baked_version
            }
        )
        if use_system_core:
            # If asked to use a globally installed tk-core instead of the one
            # specified by the config, we remove the local copy which was created
            # in the scaffold step.
            logger.info("Removing core reference in %s" % install_path)
            wipe_folder(os.path.join(install_path, "install"))
            # And make sure we don't have any reference to a tk-core in the config,
            # otherwise it would be picked up when bootstrapping.
            filesystem.safe_delete_file(os.path.join(install_path, "config", "core", "core_api.yml"))
        else:
            # Workaround for tk-core bootstrap needing a shotgun.yml file: when swapping
            # tk-core, this file is checked to see if a script user was specified and
            # should be used in place of the authenticated user. So we create a dummy
            # file with an "unspecified" host, as the key is required by the tk-core
            # code parsing the file.
            # It is not clear if this workaround is needed for non baked configs as
            # their workflow is different, so for now we just keep it for bake configs
            # only.
            shotgun_yaml_path = os.path.join(install_path, "config", "core", "shotgun.yml")
            if not os.path.exists(shotgun_yaml_path):
                logger.info("Patching %s" % shotgun_yaml_path)
                with open(shotgun_yaml_path, "w") as pf:
                    pf.write("# Workaround for tk-core bootstrap\nhost: unspecified")
    else:

        # if the descriptor in the config contains a version number
        # we will go into a fixed update mode.
        using_latest_config = is_descriptor_version_missing(base_config_uri_dict)
        if using_latest_config:
            logger.info(
                "Your configuration definition does not contain a version number. "
                "This means that the plugin will attempt to auto update at startup."
            )
        else:
            logger.info(
                "Your configuration definition contains a version number. "
                "This means that the plugin will be frozen and no automatic updates "
                "will be performed at startup."
            )

        cfg_descriptor = create_descriptor(
            sg_connection,
            Descriptor.CONFIG,
            base_config_uri_dict,
            resolve_latest=using_latest_config
        )
    logger.info("Resolved config %r" % cfg_descriptor)
    logger.info("Runtime config descriptor uri will be %s" % base_config_uri_str)
    if install_path:
        logger.info("The config was baked in %s" % install_path)
    return cfg_descriptor, base_config_uri_str, install_path