diff --git a/avalon/io.py b/avalon/io.py index 5243660c9..ff918ac12 100644 --- a/avalon/io.py +++ b/avalon/io.py @@ -114,6 +114,9 @@ def _from_environment(): session = { item[0]: os.getenv(item[0], item[1]) for item in ( + # The schema name that should be used to validate this session + ("AVALON_SESSION_SCHEMA", "avalon-core:session-2.0"), + # Root directory of projects on disk ("AVALON_PROJECTS", None), @@ -132,6 +135,9 @@ def _from_environment(): # Name of current app ("AVALON_APP", None), + # Full name of current app (e.g. versioned name) + ("AVALON_APP_NAME", None), + # Path to working directory ("AVALON_WORKDIR", None), @@ -199,7 +205,7 @@ def _from_environment(): ) if os.getenv(item[0], item[1]) is not None } - session["schema"] = "avalon-core:session-2.0" + session["schema"] = session["AVALON_SESSION_SCHEMA"] try: schema.validate(session) except schema.ValidationError as e: diff --git a/avalon/maya/commands.py b/avalon/maya/commands.py index c2eab8b51..8987edc0a 100644 --- a/avalon/maya/commands.py +++ b/avalon/maya/commands.py @@ -14,8 +14,15 @@ def reset_frame_range(): """Set frame range to current asset""" - shot = api.Session["AVALON_ASSET"] - shot = io.find_one({"name": shot, "type": "asset"}) + shot_name = api.Session.get("AVALON_ASSET") + if shot_name is None: + cmds.warning("No AVALON_ASSET setup in current working session.") + return + + shot = io.find_one({"name": shot_name, "type": "asset"}) + if shot is None: + cmds.error("Shot '%s' not found in database." % shot_name) + return try: diff --git a/avalon/maya/pipeline.py b/avalon/maya/pipeline.py index ba4005b99..73e1cfa71 100644 --- a/avalon/maya/pipeline.py +++ b/avalon/maya/pipeline.py @@ -125,8 +125,8 @@ def deferred(): # Create context menu context_label = "{}, {}".format( - api.Session["AVALON_ASSET"], - api.Session["AVALON_TASK"] + api.Session.get("AVALON_ASSET", "--"), + api.Session.get("AVALON_TASK", "--") ) cmds.menuItem( @@ -275,8 +275,8 @@ def _update_menu_task_label(): logger.warning("Can't find menuItem: {}".format(object_name)) return - label = "{}, {}".format(api.Session["AVALON_ASSET"], - api.Session["AVALON_TASK"]) + label = "{}, {}".format(api.Session.get("AVALON_ASSET", "--"), + api.Session.get("AVALON_TASK", "--")) cmds.menuItem(object_name, edit=True, label=label) diff --git a/avalon/nuke/command.py b/avalon/nuke/command.py index 58106995a..2a571723d 100644 --- a/avalon/nuke/command.py +++ b/avalon/nuke/command.py @@ -18,8 +18,16 @@ def reset_frame_range(): fps = float(api.Session.get("AVALON_FPS", 25)) nuke.root()["fps"].setValue(fps) - name = api.Session["AVALON_ASSET"] + name = api.Session.get("AVALON_ASSET") + if name is None: + log.warning("No AVALON_ASSET setup in current working session.") + return + asset = io.find_one({"name": name, "type": "asset"}) + if asset is None: + log.error("No AVALON_ASSET setup in current working session.") + return + asset_data = asset["data"] handles = get_handles(asset) diff --git a/avalon/nuke/pipeline.py b/avalon/nuke/pipeline.py index 149675c15..35cd7bbb0 100644 --- a/avalon/nuke/pipeline.py +++ b/avalon/nuke/pipeline.py @@ -274,7 +274,8 @@ def _install_menu(): menu = menubar.addMenu(api.Session["AVALON_LABEL"]) label = "{0}, {1}".format( - api.Session["AVALON_ASSET"], api.Session["AVALON_TASK"] + api.Session.get("AVALON_ASSET", "--"), + api.Session.get("AVALON_TASK", "--") ) context_action = menu.addCommand(label) context_action.setEnabled(False) diff --git a/avalon/pipeline.py b/avalon/pipeline.py index c1d9f1f18..e79292176 100644 --- a/avalon/pipeline.py +++ b/avalon/pipeline.py @@ -59,7 +59,7 @@ def install(host): io.install() missing = list() - for key in ("AVALON_PROJECT", "AVALON_ASSET"): + for key in ("AVALON_PROJECT", ): if key not in Session: missing.append(key) @@ -72,6 +72,11 @@ def install(host): project = Session["AVALON_PROJECT"] log.info("Activating %s.." % project) + if not os.getenv("_AVALON_APP_INITIALIZED"): + log.info("Initializing working directory..") + no_predefined_workdir = "AVALON_WORKDIR" not in Session + initialize(Session, reset_workdir=no_predefined_workdir) + config = find_config() # Optional host install function @@ -94,6 +99,35 @@ def install(host): log.info("Successfully installed Avalon!") +def initialize(session, reset_workdir): + """Initialize Work Directory + + This finds the current AVALON_APP_NAME and tries to triggers its + `.toml` initialization step. Note that this will only be valid + whenever `AVALON_APP_NAME` is actually set in the current session. + + """ + # Find the application definition + app_name = session.get("AVALON_APP_NAME") + if not app_name: + log.error("No AVALON_APP_NAME session variable is set. " + "Unable to initialize app Work Directory.") + return + + app_definition = lib.get_application(app_name) + App = type( + "app_%s" % app_name, + (Application,), + { + "name": app_name, + "config": app_definition.copy() + } + ) + app = App() + env = app.environ(session, reset_workdir) + app.initialize(env) + + def find_config(): log.info("Finding configuration for project..") @@ -344,7 +378,7 @@ def is_compatible(self, session): return False return True - def environ(self, session): + def environ(self, session, reset_workdir=True): """Build application environment""" session = session.copy() @@ -352,10 +386,11 @@ def environ(self, session): session["AVALON_APP_NAME"] = self.name # Compute work directory - project = io.find_one({"type": "project"}) - template = project["config"]["template"]["work"] - workdir = _format_work_template(template, session) - session["AVALON_WORKDIR"] = os.path.normpath(workdir) + if reset_workdir: + project = io.find_one({"type": "project"}) + template = project["config"]["template"]["work"] + workdir = _format_work_template(template, session) + session["AVALON_WORKDIR"] = os.path.normpath(workdir) # Construct application environment from .toml config app_environment = self.config.get("environment", {}) @@ -391,24 +426,24 @@ def initialize(self, environment): workdir = environment["AVALON_WORKDIR"] workdir_existed = os.path.exists(workdir) if not workdir_existed: - os.makedirs(workdir) self.log.info("Creating working directory '%s'" % workdir) + os.makedirs(workdir) - # Create default directories from app configuration - default_dirs = self.config.get("default_dirs", []) - default_dirs = self._format(default_dirs, **environment) - if default_dirs: - self.log.debug("Creating default directories..") - for dirname in default_dirs: - try: - os.makedirs(os.path.join(workdir, dirname)) - self.log.debug(" - %s" % dirname) - except OSError as e: - # An already existing default directory is fine. - if e.errno == errno.EEXIST: - pass - else: - raise + # Create default directories from app configuration + default_dirs = self.config.get("default_dirs", []) + default_dirs = self._format(default_dirs, **environment) + if default_dirs: + self.log.debug("Creating default directories..") + for dirname in default_dirs: + try: + os.makedirs(os.path.join(workdir, dirname)) + self.log.debug(" - %s" % dirname) + except OSError as e: + # An already existing default directory is fine. + if e.errno == errno.EEXIST: + pass + else: + raise # Perform application copy for src, dst in self.config.get("copy", {}).items(): @@ -416,6 +451,9 @@ def initialize(self, environment): # Expand env vars src, dst = self._format([src, dst], **environment) + if os.path.isfile(dst): + continue + try: self.log.info("Copying %s -> %s" % (src, dst)) shutil.copy(src, dst) @@ -423,7 +461,7 @@ def initialize(self, environment): self.log.error("Could not copy application file: %s" % e) self.log.error(" - %s -> %s" % (src, dst)) - def launch(self, environment): + def launch(self, environment, initialized=False): executable = lib.which(self.config["executable"]) if executable is None: @@ -432,6 +470,9 @@ def launch(self, environment): % (self.config["executable"], os.getenv("PATH")) ) + if initialized: + environment["_AVALON_APP_INITIALIZED"] = "1" + args = self.config.get("args", []) return lib.launch( executable=executable, @@ -444,12 +485,14 @@ def process(self, session, **kwargs): """Process the full Application action""" environment = self.environ(session) + initialized = False if kwargs.get("initialize", True): self.initialize(environment) + initialized = True if kwargs.get("launch", True): - return self.launch(environment) + return self.launch(environment, initialized) def _format(self, original, **kwargs): """Utility recursive dict formatting that logs the error clearly.""" diff --git a/avalon/schema/session-3.0.json b/avalon/schema/session-3.0.json new file mode 100644 index 000000000..4513e6692 --- /dev/null +++ b/avalon/schema/session-3.0.json @@ -0,0 +1,146 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + + "title": "avalon-core:session-3.0", + "description": "The Avalon environment", + + "type": "object", + + "additionalProperties": true, + + "required": [ + "AVALON_PROJECTS", + "AVALON_PROJECT", + "AVALON_CONFIG" + ], + + "properties": { + "AVALON_PROJECTS": { + "description": "Absolute path to root of project directories", + "type": "string", + "example": "/nas/projects" + }, + "AVALON_PROJECT": { + "description": "Name of project", + "type": "string", + "pattern": "^\\w*$", + "example": "Hulk" + }, + "AVALON_ASSET": { + "description": "Name of asset", + "type": "string", + "pattern": "^\\w*$", + "example": "Bruce" + }, + "AVALON_SILO": { + "description": "Name of asset group or container", + "type": "string", + "pattern": "^\\w*$", + "example": "assets" + }, + "AVALON_TASK": { + "description": "Name of task", + "type": "string", + "pattern": "^\\w*$", + "example": "modeling" + }, + "AVALON_CONFIG": { + "description": "Name of Avalon configuration", + "type": "string", + "pattern": "^\\w*$", + "example": "polly" + }, + "AVALON_WORKDIR": { + "description": "Current working directory of a host, such as Maya's location of workspace.mel", + "type": "string", + "example": "/mnt/projects/alita/assets/vector/maya" + }, + "AVALON_APP": { + "description": "Name of application", + "type": "string", + "pattern": "^\\w*$", + "example": "maya2016" + }, + "AVALON_MONGO": { + "description": "Address to the asset database", + "type": "string", + "pattern": "^mongodb://[\\w/@:.]*$", + "example": "mongodb://localhost:27017", + "default": "mongodb://localhost:27017" + }, + "AVALON_DB": { + "description": "Name of database", + "type": "string", + "pattern": "^\\w*$", + "example": "avalon", + "default": "avalon" + }, + "AVALON_LABEL": { + "description": "Nice name of Avalon, used in e.g. graphical user interfaces", + "type": "string", + "example": "Mindbender", + "default": "Avalon" + }, + "AVALON_SENTRY": { + "description": "Address to Sentry", + "type": "string", + "pattern": "^http[\\w/@:.]*$", + "example": "https://5b872b280de742919b115bdc8da076a5:8d278266fe764361b8fa6024af004a9c@logs.mindbender.com/2", + "default": null + }, + "AVALON_DEADLINE": { + "description": "Address to Deadline", + "type": "string", + "pattern": "^http[\\w/@:.]*$", + "example": "http://192.168.99.101", + "default": null + }, + "AVALON_TIMEOUT": { + "description": "Wherever there is a need for a timeout, this is the default value.", + "type": "string", + "pattern": "^[0-9]*$", + "default": "1000", + "example": "1000" + }, + "AVALON_UPLOAD": { + "description": "Boolean of whether to upload published material to central asset repository", + "type": "string", + "default": null, + "example": "True" + }, + "AVALON_USERNAME": { + "description": "Generic username", + "type": "string", + "pattern": "^\\w*$", + "default": "avalon", + "example": "myself" + }, + "AVALON_PASSWORD": { + "description": "Generic password", + "type": "string", + "pattern": "^\\w*$", + "default": "secret", + "example": "abc123" + }, + "AVALON_INSTANCE_ID": { + "description": "Unique identifier for instances in a working file", + "type": "string", + "pattern": "^[\\w.]*$", + "default": "avalon.instance", + "example": "avalon.instance" + }, + "AVALON_CONTAINER_ID": { + "description": "Unique identifier for a loaded representation in a working file", + "type": "string", + "pattern": "^[\\w.]*$", + "default": "avalon.container", + "example": "avalon.container" + }, + "AVALON_DEBUG": { + "description": "Enable debugging mode. Some applications may use this for e.g. extended verbosity or mock plug-ins.", + "type": "string", + "default": null, + "example": "True" + } + } +} diff --git a/avalon/tools/creator/app.py b/avalon/tools/creator/app.py index 25c79c1d6..ea2e0a00d 100644 --- a/avalon/tools/creator/app.py +++ b/avalon/tools/creator/app.py @@ -396,7 +396,7 @@ def refresh(self): listing = self.data["Listing"] asset = self.data["Asset"] - asset.setText(api.Session["AVALON_ASSET"]) + asset.setText(api.Session.get("AVALON_ASSET", "")) has_families = False diff --git a/avalon/tools/loader/app.py b/avalon/tools/loader/app.py index a0fd8337a..6b418b86a 100644 --- a/avalon/tools/loader/app.py +++ b/avalon/tools/loader/app.py @@ -430,7 +430,7 @@ def show(debug=False, parent=None, use_context=False): window.setStyleSheet(style.load_stylesheet()) if use_context: - context = {"asset": api.Session["AVALON_ASSET"]} + context = {"asset": api.Session.get("AVALON_ASSET")} window.set_context(context, refresh=True) else: window.refresh() diff --git a/avalon/tools/workfiles/app.py b/avalon/tools/workfiles/app.py index 70c614de7..a0d3a7841 100644 --- a/avalon/tools/workfiles/app.py +++ b/avalon/tools/workfiles/app.py @@ -651,23 +651,8 @@ def initialize_work_directory(self): asset=self._asset, task=self._task) session.update(changes) - - # Find the application definition - app_name = os.environ.get("AVALON_APP_NAME") - if not app_name: - log.error("No AVALON_APP_NAME session variable is set. " - "Unable to initialize app Work Directory.") - return - - app_definition = pipeline.lib.get_application(app_name) - App = type("app_%s" % app_name, - (pipeline.Application,), - {"config": app_definition.copy()}) - # Initialize within the new session's environment - app = App() - env = app.environ(session) - app.initialize(env) + pipeline.initialize(session, reset_workdir=True) # Force a full to the asset as opposed to just self.refresh() so # that it will actually check again whether the Work directory exists @@ -805,7 +790,7 @@ def on_asset_changed(self): def set_context(self, context): - if "asset" in context: + if context.get("asset"): asset = context["asset"] asset_document = io.find_one({ "name": asset, @@ -818,7 +803,7 @@ def set_context(self, context): # Force a refresh on Tasks? self.widgets["tasks"].set_asset(asset_document) - if "task" in context: + if context.get("task"): self.widgets["tasks"].select_task(context["task"]) def refresh(self): @@ -892,9 +877,8 @@ def show(root=None, debug=False, parent=None, use_context=True): window.refresh() if use_context: - context = {"asset": api.Session["AVALON_ASSET"], - "silo": api.Session["AVALON_SILO"], - "task": api.Session["AVALON_TASK"]} + context = {"asset": api.Session.get("AVALON_ASSET"), + "task": api.Session.get("AVALON_TASK")} window.set_context(context) window.show() diff --git a/res/houdini/MainMenuCommon.XML b/res/houdini/MainMenuCommon.XML index a492ef7cd..c72ece8a0 100644 --- a/res/houdini/MainMenuCommon.XML +++ b/res/houdini/MainMenuCommon.XML @@ -6,7 +6,8 @@