Skip to content

Commit

Permalink
application: refactor to work with introduced snap meta objects
Browse files Browse the repository at this point in the history
- Add from_dict() method to instantiate app from dict.
- Remove requirement for prime_dir and base at init-time.
- Add populate_commands() to initialize commands with
  prime_dir and base as parameters.
- Rename _verify_paths to validate_command_chain_executables(),
  requiring prime_dir to check against.
- Rename get_yaml() to to_dict()
- Normalize yaml dict in to_dict() rather than popping keys
  from app_properties in init.
- Make adapter, desktop_file, commands, and command_chain public
  properties.
- Update application tests.

Signed-off-by: Chris Patterson <chris.patterson@canonical.com>
  • Loading branch information
Chris Patterson authored and sergiusens committed Oct 7, 2019
1 parent 18fd2f4 commit 83e94d3
Show file tree
Hide file tree
Showing 2 changed files with 185 additions and 141 deletions.
199 changes: 126 additions & 73 deletions snapcraft/internal/meta/application.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,15 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.

import os

from copy import deepcopy
from typing import Any, Dict, Optional, Sequence
from snapcraft import yaml_utils
from typing import Any, Dict, List, Optional, Sequence # noqa: F401

from . import errors
from ._utils import _executable_is_valid
from .command import Command
from .desktop import DesktopFile
from snapcraft import yaml_utils


_COMMAND_ENTRIES = ["command", "stop-command"]
Expand All @@ -36,116 +37,168 @@ def __init__(
self,
*,
app_name: str,
app_properties: Dict[str, Any],
base: str,
prime_dir: str
app_properties: Dict[str, Any] = None,
adapter: str = None,
desktop: str = None,
command_chain: List[str] = None,
prepend_command_chain: List[str] = None,
passthrough: Dict[str, Any] = None,
commands: Dict[str, Command] = None
) -> None:
"""Initialize an application entry.
:param str app_name: the name of the application.
:param dict app_properties: the snapcraft.yaml application entry under
apps.
:param str prime_dir: directory containing the final assets to a snap.
:param str base:
TODO: Eliminate use of app_properties.
"""
self._app_name = app_name
self._app_properties = deepcopy(app_properties)
self._base = base
self._prime_dir = prime_dir

# App Properties that should not make it to snap.yaml
self._adapter = self._app_properties.pop("adapter", None)
self._desktop_file = self._app_properties.pop("desktop", None)
self._app_properties = app_properties
if self._app_properties is None:
self._app_properties: Dict[str, Any] = dict()

self._commands = self._get_commands()
self._verify_paths()
self.adapter = adapter
self.desktop = desktop

def _is_adapter(self, adapter: str) -> bool:
if self._adapter is None:
return False
self.command_chain = command_chain
if self.command_chain is None:
self.command_chain: List[str] = list()

return self._adapter == adapter
self.prepend_command_chain = prepend_command_chain
if self.prepend_command_chain is None:
self.prepend_command_chain: List[str] = list()

def _can_use_wrapper(self) -> bool:
self.commands = commands
if self.commands is None:
self.commands: Dict[str, Command] = dict()

self.passthrough = passthrough
if self.passthrough is None:
self.passthrough: Dict[str, Any] = dict()

@property
def app_name(self) -> str:
"""Read-only to ensure consistency with Snap dictionary mappings."""
return self._app_name

def can_use_wrapper(self, base: str) -> bool:
"""Return if an wrapper should be allowed for app entries."""
# Force use of no wrappers when command-chain is set.
if "command-chain" in self._app_properties:
if self.command_chain:
return False

# We only allow wrappers for core and core18.
if self._base not in _MASSAGED_BASES:
if not self._massage_commands(base=base):
return False

# Now that command-chain and bases have been checked for,
# check if the none adapter has been forced.
if self._is_adapter("none"):
if self.adapter == "none":
return False

return True

def _get_commands(self) -> Dict[str, Command]:
can_use_wrapper = self._can_use_wrapper()
commands = dict()
for c in _COMMAND_ENTRIES:
if c in self._app_properties:
commands[c] = Command(
app_name=self._app_name,
command_name=c,
command=self._app_properties[c],
prime_dir=self._prime_dir,
can_use_wrapper=can_use_wrapper,
massage_command=self._base in _MASSAGED_BASES,
)

return commands

def _verify_paths(self) -> None:
for item in self._app_properties.get("command-chain", []):
executable_path = os.path.join(self._prime_dir, item)
def _massage_commands(self, *, base: str) -> bool:
return base in _MASSAGED_BASES

def prime_commands(self, *, base: str, prime_dir: str) -> None:
can_use_wrapper = self.can_use_wrapper(base)
massage_command = self._massage_commands(base=base)
for command in self.commands.values():
command.prime_command(
can_use_wrapper=can_use_wrapper,
massage_command=massage_command,
prime_dir=prime_dir,
)

# command-chain entries must always be relative to the root of
# the snap, i.e. PATH is not used.
if not _executable_is_valid(executable_path):
raise errors.InvalidCommandChainError(item, self._app_name)
def write_command_wrappers(self, *, prime_dir: str) -> None:
for command in self.commands.values():
command.write_wrapper(prime_dir=prime_dir)

def generate_desktop_file(
self, *, snap_name: str, gui_dir: str, icon_path: Optional[str] = None
def write_application_desktop_file(
self, snap_name: str, prime_dir: str, gui_dir: str, icon_path: Optional[str]
) -> None:
if self._desktop_file is None:
if not self.desktop:
return

desktop_file = DesktopFile(
snap_name=snap_name,
app_name=self._app_name,
filename=self._desktop_file,
prime_dir=self._prime_dir,
app_name=self.app_name,
filename=self.desktop,
prime_dir=prime_dir,
)

desktop_file.write(gui_dir=gui_dir, icon_path=icon_path)

def generate_command_wrappers(self) -> None:
for command in self._commands.values():
command.generate_wrapper()
def validate_command_chain_executables(self, prime_dir: str) -> None:
for item in self.command_chain:
executable_path = os.path.join(prime_dir, item)

# command-chain entries must always be relative to the root of
# the snap, i.e. PATH is not used.
if not _executable_is_valid(executable_path):
raise errors.InvalidCommandChainError(item, self.app_name)

def validate(self) -> None:
"""Validate application, raising exception on error."""

# No checks here (yet).
return

@classmethod
def from_dict(cls, *, app_dict: Dict[str, Any], app_name: str) -> "Application":
"""Create application from dictionary."""

app_dict = deepcopy(app_dict)
app = Application(
app_name=app_name,
app_properties=app_dict,
adapter=app_dict.get("adapter", None),
desktop=app_dict.get("desktop", None),
command_chain=app_dict.get("command-chain", None),
passthrough=app_dict.get("passthrough", None),
)

# Populate commands from app_properties.
for command_name in _COMMAND_ENTRIES:
if command_name not in app_dict:
continue

app.commands[command_name] = Command(
app_name=app_name,
command_name=command_name,
command=app_dict[command_name],
)

return app

def to_dict(self) -> Dict[str, Any]:
"""Returns and ordered dictonary with the transformed app entry."""

app_dict = deepcopy(self._app_properties)

for command_name, command in self.commands.items():
app_dict[command_name] = command.command

if self.prepend_command_chain or self.command_chain:
app_dict["command-chain"] = self.prepend_command_chain + self.command_chain

def _fix_sockets(self) -> None:
# Adjust socket values to formats snap.yaml accepts
sockets = self._app_properties.get("sockets", dict())
sockets = app_dict.get("sockets", dict())
for socket in sockets.values():
mode = socket.get("socket-mode")
if mode is not None:
socket["socket-mode"] = yaml_utils.OctInt(mode)

def get_yaml(self, *, prepend_command_chain: Sequence[str]) -> Dict[str, Any]:
"""Returns and ordered dictonary with the transformed app entry."""
for command_entry, command in self._commands.items():
self._app_properties[command_entry] = command.get_command()

if "command-chain" in self._app_properties or prepend_command_chain:
self._app_properties[
"command-chain"
] = prepend_command_chain + self._app_properties.get(
"command-chain", list()
)
# Strip keys.
for key in ["adapter", "desktop"]:
if key in app_dict:
app_dict.pop(key)

# Apply passthrough keys.
app_dict.update(self.passthrough)
return app_dict

self._fix_sockets()
def __repr__(self) -> str:
return repr(self.__dict__)

return self._app_properties
def __str__(self) -> str:
return str(self.__dict__)
Loading

0 comments on commit 83e94d3

Please sign in to comment.