Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
83 changes: 83 additions & 0 deletions docs/descriptor.rst
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,8 @@ Several different descriptor types are supported by Toolkit:
- A **git** descriptor represents a tag in a git repository
- A **git_branch** descriptor represents a commit in a git branch
- A **github_release** descriptor represents a Release on a Github repo
- A **perforce_change** descriptor represents a changelist in a Perforce depot
- A **perforce_label** descriptor represents a Label in a Perforce depot
- A **path** descriptor represents a location on disk
- A **dev** descriptor represents a developer sandbox
- A **manual** descriptor gives raw access to the bundle caching structure
Expand Down Expand Up @@ -343,6 +345,87 @@ A token must be set as environment variable that is specific to the organization
.. note:: For private repos, it's recommended that you use a personal access token (classic) with read-only access to Content. Fine-grained tokens are not yet supported. For more information, see the `Github Documentation on Personal Access Tokens <https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/creating-a-personal-access-token>`_.


Tracking against changelist in Perforce
=======================================

The ``perforce_change`` descriptor type is useful for studios and 3rd parties wishing to deploy apps directly from their
Perforce server.
This ``print's`` all the files from depot at the supplied path at the revision of the supplied changelist.
Connects to the perforce server via command line. Prior setup of perforce is and user should be login
as usual if authenticating is required on your server. This also supports remote over a VPN connection.

Getting ``tk-multi-loader2`` from ``//DEPOT/AppStore/tk-multi-loader2`` in Perforce:

.. code-block:: yaml

{
type: perforce_change,
path: //DEPOT/AppStore/tk-multi-loader2
changelist: 12345
}


Use latest:

.. code-block:: yaml

{
type: perforce_change,
path: //DEPOT/AppStore/tk-multi-loader2
}

Environment variable support:

.. code-block:: yaml

{
type: perforce_change,
path: ${DEPOT_APPSTORE}/tk-multi-loader2
}

.. code-block:: yaml

sgtk:descriptor:perforce_change?path=//DEPOT/AppStore/tk-multi-loader2&changelist=12345

- If ``changelist`` is not supplied, the latest will be fetched.
- ``path`` is the depot path to where the code is stored.
- ``changelist`` is the changelist number in the depot.

.. _perforce_descriptors:

Tracking against Labels in Perforce
===================================

The ``perforce_label`` descriptor ``print's`` all the files from depot at the
supplied path at the revision of the supplied changelist.

Getting ``tk-multi-loader2`` from ``//DEPOT/AppStore/tk-multi-loader2`` in Perforce:

.. code-block:: yaml

{
type: perforce_label
path: //DEPOT/AppStore/tk-multi-loader2
label: v3.0.0
}
.. code-block:: yaml

{
type: perforce_label,
path: //DEPOT/AppStore/tk-multi-loader2
label: tk-multi-loader2-v3.0.0
}

.. code-block:: yaml

sgtk:descriptor:perforce_label?path=//DEPOT/AppStore/tk-multi-loader2&label=v3.0.0

- ``path`` is the depot path to where the code is stored.
- ``label`` is the Label tag given to the depot path.

.. note:: If you want constraint patterns (i.e. ``v1.x.x``) to work correctly with this descriptor, you must follow the `semantic versioning <https://semver.org/>`_ specification when naming Labels in Perforce.


Pointing to a path on disk
==========================

Expand Down
8 changes: 8 additions & 0 deletions python/tank/descriptor/io_descriptor/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ def _initialize_descriptor_factory():
from .git_tag import IODescriptorGitTag
from .git_branch import IODescriptorGitBranch
from .github_release import IODescriptorGithubRelease
from .perforce_change import IODescriptorPerforceChange
from .perforce_label import IODescriptorPerforceLabel
from .manual import IODescriptorManual

IODescriptorBase.register_descriptor_factory("app_store", IODescriptorAppStore)
Expand All @@ -41,6 +43,12 @@ def _initialize_descriptor_factory():
IODescriptorBase.register_descriptor_factory(
"github_release", IODescriptorGithubRelease
)
IODescriptorBase.register_descriptor_factory(
"perforce_change", IODescriptorPerforceChange
)
IODescriptorBase.register_descriptor_factory(
"perforce_label", IODescriptorPerforceLabel
)
IODescriptorBase.register_descriptor_factory("manual", IODescriptorManual)


Expand Down
237 changes: 237 additions & 0 deletions python/tank/descriptor/io_descriptor/perforce.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,237 @@
# Copyright (c) 2016 Shotgun Software Inc.
#
# CONFIDENTIAL AND PROPRIETARY
#
# This work is provided "AS IS" and subject to the Shotgun Pipeline Toolkit
# Source Code License included in this distribution package. See LICENSE.
# By accessing, using, copying or modifying this work you indicate your
# agreement to the Shotgun Pipeline Toolkit Source Code License. All rights
# not expressly granted therein are reserved by Shotgun Software Inc.
import os
import subprocess

Check notice on line 11 in python/tank/descriptor/io_descriptor/perforce.py

View check run for this annotation

ShotGrid Chorus / security/bandit

B404: blacklist

Consider possible security implications associated with the subprocess module. secure coding id: PYTH-INJC-30.

from .downloadable import IODescriptorDownloadable
from ... import LogManager
from ...util.process import subprocess_check_output, SubprocessCalledProcessError

from ..errors import TankError
from ...util import filesystem
from ...util import is_windows

log = LogManager.get_logger(__name__)


def _can_hide_terminal():
"""
Ensures this version of Python can hide the terminal of a subprocess
launched with the subprocess module.
"""
try:

Check warning on line 29 in python/tank/descriptor/io_descriptor/perforce.py

View check run for this annotation

Codecov / codecov/patch

python/tank/descriptor/io_descriptor/perforce.py#L29

Added line #L29 was not covered by tests
# These values are not defined between Python 2.6.6 and 2.7.1 inclusively.
subprocess.STARTF_USESHOWWINDOW
subprocess.SW_HIDE
return True
except Exception:
return False

Check warning on line 35 in python/tank/descriptor/io_descriptor/perforce.py

View check run for this annotation

Codecov / codecov/patch

python/tank/descriptor/io_descriptor/perforce.py#L31-L35

Added lines #L31 - L35 were not covered by tests


def _check_output(*args, **kwargs):
"""
Wraps the call to subprocess_check_output so it can run headless on Windows.
"""
if is_windows() and _can_hide_terminal():
startupinfo = subprocess.STARTUPINFO()
startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
startupinfo.wShowWindow = subprocess.SW_HIDE
kwargs["startupinfo"] = startupinfo

Check warning on line 46 in python/tank/descriptor/io_descriptor/perforce.py

View check run for this annotation

Codecov / codecov/patch

python/tank/descriptor/io_descriptor/perforce.py#L42-L46

Added lines #L42 - L46 were not covered by tests

return subprocess_check_output(*args, **kwargs)

Check warning on line 48 in python/tank/descriptor/io_descriptor/perforce.py

View check run for this annotation

Codecov / codecov/patch

python/tank/descriptor/io_descriptor/perforce.py#L48

Added line #L48 was not covered by tests


class TankPerforceError(TankError):
"""
Errors related to p4 communication
"""

pass


class IODescriptorPerforce(IODescriptorDownloadable):
"""
Base class for perforce descriptors.

Abstracts operations around depots, since all p4
descriptors have a repository associated (via the 'path'
parameter).
"""

def __init__(self, descriptor_dict, sg_connection, bundle_type):
"""
Constructor

:param descriptor_dict: descriptor dictionary describing the bundle
:param sg_connection: Shotgun connection to associated site.
:param bundle_type: Either AppDescriptor.APP, CORE, ENGINE or FRAMEWORK.
:return: Descriptor instance
"""

self._cache_type = "perforce"

Check warning on line 78 in python/tank/descriptor/io_descriptor/perforce.py

View check run for this annotation

Codecov / codecov/patch

python/tank/descriptor/io_descriptor/perforce.py#L78

Added line #L78 was not covered by tests

super(IODescriptorPerforce, self).__init__(

Check warning on line 80 in python/tank/descriptor/io_descriptor/perforce.py

View check run for this annotation

Codecov / codecov/patch

python/tank/descriptor/io_descriptor/perforce.py#L80

Added line #L80 was not covered by tests
descriptor_dict, sg_connection, bundle_type
)

self._path = descriptor_dict.get("path")

Check warning on line 84 in python/tank/descriptor/io_descriptor/perforce.py

View check run for this annotation

Codecov / codecov/patch

python/tank/descriptor/io_descriptor/perforce.py#L84

Added line #L84 was not covered by tests
# Expand environment variables for depot roots
self._path = os.path.expandvars(self._path)
self._path = os.path.expanduser(self._path)

Check warning on line 87 in python/tank/descriptor/io_descriptor/perforce.py

View check run for this annotation

Codecov / codecov/patch

python/tank/descriptor/io_descriptor/perforce.py#L86-L87

Added lines #L86 - L87 were not covered by tests

# strip trailing slashes - this is so that when we build
# the name later (using os.basename) we construct it correctly.
if self._path.endswith("/") or self._path.endswith("\\"):
self._path = self._path[:-1]

Check warning on line 92 in python/tank/descriptor/io_descriptor/perforce.py

View check run for this annotation

Codecov / codecov/patch

python/tank/descriptor/io_descriptor/perforce.py#L91-L92

Added lines #L91 - L92 were not covered by tests

@LogManager.log_timing
def execute_p4_commands(self, target_path, commands):
"""
Downloads the depot path into the given location

The initial sync operation happens via the subprocess module, ensuring
there is no terminal that will pop for credentials, leading to a more
seamless experience. If the operation failed, we try a second time with
os.system, ensuring that there is an initialized shell environment

:param target_path: path to clone into
:param commands: list p4 commands to execute, e.g. ['p4 x']
:returns: stdout and stderr of the last command executed as a string
:raises: TankPerforceError on p4 failure
"""
# ensure *parent* folder exists
parent_folder = os.path.dirname(target_path)

Check warning on line 110 in python/tank/descriptor/io_descriptor/perforce.py

View check run for this annotation

Codecov / codecov/patch

python/tank/descriptor/io_descriptor/perforce.py#L110

Added line #L110 was not covered by tests

filesystem.ensure_folder_exists(parent_folder)

Check warning on line 112 in python/tank/descriptor/io_descriptor/perforce.py

View check run for this annotation

Codecov / codecov/patch

python/tank/descriptor/io_descriptor/perforce.py#L112

Added line #L112 was not covered by tests

# first probe to check that p4 exists in our PATH
log.debug("Checking that p4 exists and can be executed...")
try:
output = _check_output(["p4", "info"])
except Exception:
log.exception("Unexpected error:")
raise TankPerforceError(

Check warning on line 120 in python/tank/descriptor/io_descriptor/perforce.py

View check run for this annotation

Codecov / codecov/patch

python/tank/descriptor/io_descriptor/perforce.py#L115-L120

Added lines #L115 - L120 were not covered by tests
"Cannot execute the 'p4' command. Please make sure that p4 is "
"installed on your system."
)

run_with_os_system = True

Check warning on line 125 in python/tank/descriptor/io_descriptor/perforce.py

View check run for this annotation

Codecov / codecov/patch

python/tank/descriptor/io_descriptor/perforce.py#L125

Added line #L125 was not covered by tests

output = None
if is_windows() and _can_hide_terminal():
log.debug("Executing command '%s' using subprocess module." % commands)
try:
environ = {}
environ.update(os.environ)
output = _check_output(commands, env=environ)

Check warning on line 133 in python/tank/descriptor/io_descriptor/perforce.py

View check run for this annotation

Codecov / codecov/patch

python/tank/descriptor/io_descriptor/perforce.py#L127-L133

Added lines #L127 - L133 were not covered by tests

log.debug("p4 output %s" % output)

Check warning on line 135 in python/tank/descriptor/io_descriptor/perforce.py

View check run for this annotation

Codecov / codecov/patch

python/tank/descriptor/io_descriptor/perforce.py#L135

Added line #L135 was not covered by tests

# If that works, we're done and we don't need to use os.system.
run_with_os_system = False
status = 0
except SubprocessCalledProcessError:
log.debug("Subprocess call failed.")

Check warning on line 141 in python/tank/descriptor/io_descriptor/perforce.py

View check run for this annotation

Codecov / codecov/patch

python/tank/descriptor/io_descriptor/perforce.py#L138-L141

Added lines #L138 - L141 were not covered by tests

if run_with_os_system:

Check warning on line 143 in python/tank/descriptor/io_descriptor/perforce.py

View check run for this annotation

Codecov / codecov/patch

python/tank/descriptor/io_descriptor/perforce.py#L143

Added line #L143 was not covered by tests
# Make sure path and repo path are quoted.
log.debug("Executing command '%s' using os.system" % commands)
log.debug(

Check warning on line 146 in python/tank/descriptor/io_descriptor/perforce.py

View check run for this annotation

Codecov / codecov/patch

python/tank/descriptor/io_descriptor/perforce.py#L145-L146

Added lines #L145 - L146 were not covered by tests
"Note: in a terminal environment, this may prompt for authentication"
)
status = os.system(" ".join(commands))

Check failure on line 149 in python/tank/descriptor/io_descriptor/perforce.py

View check run for this annotation

ShotGrid Chorus / security/bandit

B605: start_process_with_a_shell

Starting a process with a shell, possible injection detected, security issue. secure coding id: PYTH-INJC-30.

Check warning on line 149 in python/tank/descriptor/io_descriptor/perforce.py

View check run for this annotation

Codecov / codecov/patch

python/tank/descriptor/io_descriptor/perforce.py#L149

Added line #L149 was not covered by tests

log.debug("Command returned exit code %s" % status)
if status != 0:
raise TankPerforceError(

Check warning on line 153 in python/tank/descriptor/io_descriptor/perforce.py

View check run for this annotation

Codecov / codecov/patch

python/tank/descriptor/io_descriptor/perforce.py#L151-L153

Added lines #L151 - L153 were not covered by tests
"Error executing p4 operation. The p4 command '%s' "
"returned error code %s." % (commands, status)
)
log.debug("P4 print into '%s' successful." % target_path)

Check warning on line 157 in python/tank/descriptor/io_descriptor/perforce.py

View check run for this annotation

Codecov / codecov/patch

python/tank/descriptor/io_descriptor/perforce.py#L157

Added line #L157 was not covered by tests

# return the last returned stdout/stderr
return output

Check warning on line 160 in python/tank/descriptor/io_descriptor/perforce.py

View check run for this annotation

Codecov / codecov/patch

python/tank/descriptor/io_descriptor/perforce.py#L160

Added line #L160 was not covered by tests

def _download_local(self, destination_path):
"""
Retrieves this version to from the depot
Will exit early if app already exists local.

This will connect to p4 depot.
The p4 depot path will be downloaded at the descriptor version (changelist or label)

:param destination_path: The destination path on disk to which
the p4 depot path is to be downloaded to.
"""
try:

Check warning on line 173 in python/tank/descriptor/io_descriptor/perforce.py

View check run for this annotation

Codecov / codecov/patch

python/tank/descriptor/io_descriptor/perforce.py#L173

Added line #L173 was not covered by tests
# Use perforce print to download the files without requiring
# workspace setup. Applies the path format to download from
# a depot path to a folder at the specified change or label.
destination_path = destination_path.replace("\\", "/")
commands = [

Check warning on line 178 in python/tank/descriptor/io_descriptor/perforce.py

View check run for this annotation

Codecov / codecov/patch

python/tank/descriptor/io_descriptor/perforce.py#L177-L178

Added lines #L177 - L178 were not covered by tests
"p4",
"print",
"-o",
"%s/..." % destination_path,
"%s/...@%s" % (self._path, self._version),
]
self.execute_p4_commands(destination_path, commands)

Check warning on line 185 in python/tank/descriptor/io_descriptor/perforce.py

View check run for this annotation

Codecov / codecov/patch

python/tank/descriptor/io_descriptor/perforce.py#L185

Added line #L185 was not covered by tests

except Exception as e:
raise TankPerforceError(

Check warning on line 188 in python/tank/descriptor/io_descriptor/perforce.py

View check run for this annotation

Codecov / codecov/patch

python/tank/descriptor/io_descriptor/perforce.py#L187-L188

Added lines #L187 - L188 were not covered by tests
"Could not download %s, "
"commit %s: %s" % (self._path, self._version, e)
)

def get_system_name(self):
"""
Returns a short name, suitable for use in configuration files
and for folders on disk, e.g. 'tk-maya'
"""
bn = os.path.basename(self._path)
(name, ext) = os.path.splitext(bn)
return name

Check warning on line 200 in python/tank/descriptor/io_descriptor/perforce.py

View check run for this annotation

Codecov / codecov/patch

python/tank/descriptor/io_descriptor/perforce.py#L198-L200

Added lines #L198 - L200 were not covered by tests

def has_remote_access(self):
"""
Probes if the current descriptor is able to handle
remote requests. If this method returns, true, operations
such as :meth:`download_local` and :meth:`get_latest_version`
can be expected to succeed.

:return: True if a remote is accessible, false if not.
"""
# check if we can clone the repo
can_connect = True
try:
log.debug("%r: Probing if a connection to p4 can be established..." % self)

Check warning on line 214 in python/tank/descriptor/io_descriptor/perforce.py

View check run for this annotation

Codecov / codecov/patch

python/tank/descriptor/io_descriptor/perforce.py#L212-L214

Added lines #L212 - L214 were not covered by tests
# clone repo into temp folder
subprocess.check_output(["p4", "info"])

Check notice on line 216 in python/tank/descriptor/io_descriptor/perforce.py

View check run for this annotation

ShotGrid Chorus / security/bandit

B607: start_process_with_partial_path

Starting a process with a partial executable path secure coding id: PYTH-INJC-30.

Check notice on line 216 in python/tank/descriptor/io_descriptor/perforce.py

View check run for this annotation

ShotGrid Chorus / security/bandit

B603: subprocess_without_shell_equals_true

subprocess call - check for execution of untrusted input. secure coding id: PYTH-INJC-30.
log.debug("...connection established")
except Exception as e:
log.debug("...could not establish connection: %s" % e)
can_connect = False
return can_connect

Check warning on line 221 in python/tank/descriptor/io_descriptor/perforce.py

View check run for this annotation

Codecov / codecov/patch

python/tank/descriptor/io_descriptor/perforce.py#L216-L221

Added lines #L216 - L221 were not covered by tests

def _get_bundle_cache_path(self, bundle_cache_root):
"""
Given a cache root, compute a cache path suitable
for this descriptor, using the 0.18+ path format.

:param bundle_cache_root: Bundle cache root path
:return: Path to bundle cache location
"""
# If the descriptor is an integer change the version to a string type
if isinstance(self._version, int):
self._version = str(self._version)

Check warning on line 233 in python/tank/descriptor/io_descriptor/perforce.py

View check run for this annotation

Codecov / codecov/patch

python/tank/descriptor/io_descriptor/perforce.py#L232-L233

Added lines #L232 - L233 were not covered by tests

name = os.path.basename(self._path)

Check warning on line 235 in python/tank/descriptor/io_descriptor/perforce.py

View check run for this annotation

Codecov / codecov/patch

python/tank/descriptor/io_descriptor/perforce.py#L235

Added line #L235 was not covered by tests

return os.path.join(bundle_cache_root, self._cache_type, name, self._version)

Check warning on line 237 in python/tank/descriptor/io_descriptor/perforce.py

View check run for this annotation

Codecov / codecov/patch

python/tank/descriptor/io_descriptor/perforce.py#L237

Added line #L237 was not covered by tests
Loading
Loading