示例#1
0
def make_symlink(src, dst):
    """Makes symlinks for directories.
    Args:
        src (str): target path, where the files are to be stored.
        dst (str): full path of link that will point to src.
    """
    if os.path.exists(dst):
        if os.path.islink(dst) and os.path.samefile(dst, src):
            # correct symlink already exists
            return False
        # symlink name is in use by a physical file or directory
        # log and return
        LOG.debug(
            f"Unable to create {src} symlink. The path {dst} already exists.")
        return False
    elif os.path.islink(dst):
        # remove a bad symlink.
        try:
            os.unlink(dst)
        except Exception:
            raise WorkflowFilesError(
                f"Error when symlinking. Failed to unlink bad symlink {dst}.")
    os.makedirs(src, exist_ok=True)
    os.makedirs(os.path.dirname(dst), exist_ok=True)
    try:
        os.symlink(src, dst, target_is_directory=True)
        return True
    except Exception as exc:
        raise WorkflowFilesError(f"Error when symlinking\n{exc}")
示例#2
0
def main(parser: COP, opts: 'Values', args: Optional[str] = None) -> None:
    run_dir: Optional[Path]
    workflow_id: str
    if args is None:
        try:
            workflow_id = str(Path.cwd().relative_to(
                Path(get_cylc_run_dir()).resolve()))
        except ValueError:
            raise WorkflowFilesError(
                "The current working directory is not a workflow run directory"
            )
    else:
        workflow_id, *_ = parse_id(
            args,
            constraint='workflows',
        )
    run_dir = Path(get_workflow_run_dir(workflow_id))
    if not run_dir.is_dir():
        raise WorkflowFilesError(
            f'"{workflow_id}" is not an installed workflow.')
    source, source_symlink = get_workflow_source_dir(run_dir)
    if not source:
        raise WorkflowFilesError(
            f'"{workflow_id}" was not installed with cylc install.')
    if not Path(source).is_dir():
        raise WorkflowFilesError(
            f'Workflow source dir is not accessible: "{source}".\n'
            f'Restore the source or modify the "{source_symlink}"'
            ' symlink to continue.')
    for entry_point in iter_entry_points('cylc.pre_configure'):
        try:
            entry_point.resolve()(srcdir=source, opts=opts)
        except Exception as exc:
            # NOTE: except Exception (purposefully vague)
            # this is to separate plugin from core Cylc errors
            raise PluginError('cylc.pre_configure', entry_point.name,
                              exc) from None

    reinstall_workflow(
        named_run=workflow_id,
        rundir=run_dir,
        source=source,
        dry_run=False  # TODO: ready for dry run implementation
    )

    for entry_point in iter_entry_points('cylc.post_install'):
        try:
            entry_point.resolve()(srcdir=source,
                                  opts=opts,
                                  rundir=str(run_dir))
        except Exception as exc:
            # NOTE: except Exception (purposefully vague)
            # this is to separate plugin from core Cylc errors
            raise PluginError('cylc.post_install', entry_point.name,
                              exc) from None
示例#3
0
def validate_flow_name(flow_name: str) -> None:
    """Check workflow name is valid and not an absolute path.

    Raise WorkflowFilesError if not valid.
    """
    is_valid, message = SuiteNameValidator.validate(flow_name)
    if not is_valid:
        raise WorkflowFilesError(
            f"invalid workflow name '{flow_name}' - {message}")
    if os.path.isabs(flow_name):
        raise WorkflowFilesError(
            f"workflow name cannot be an absolute path: {flow_name}")
示例#4
0
def main(parser: COP,
         opts: 'Options',
         named_run: Optional[str] = None) -> None:
    if not named_run:
        source, _ = get_workflow_source_dir(Path.cwd())
        if source is None:
            raise WorkflowFilesError(
                f'"{Path.cwd()}" is not a workflow run directory.')
        base_run_dir = Path(get_workflow_run_dir(''))
        named_run = str(Path.cwd().relative_to(base_run_dir.resolve()))
    run_dir = Path(get_workflow_run_dir(named_run))
    if not run_dir.exists():
        raise WorkflowFilesError(
            f'"{named_run}" is not an installed workflow.')
    if run_dir.name in [SuiteFiles.FLOW_FILE, SuiteFiles.SUITE_RC]:
        run_dir = run_dir.parent
        named_run = named_run.rsplit('/', 1)[0]
    source, source_path = get_workflow_source_dir(run_dir)
    if not source:
        raise WorkflowFilesError(
            f'"{named_run}" was not installed with cylc install.')
    if not Path(source).exists():
        raise WorkflowFilesError(
            f'Workflow source dir is not accessible: "{source}".\n'
            f'Restore the source or modify the "{source_path}"'
            ' symlink to continue.')
    for entry_point in pkg_resources.iter_entry_points('cylc.pre_configure'):
        try:
            entry_point.resolve()(srcdir=source, opts=opts)
        except Exception as exc:
            # NOTE: except Exception (purposefully vague)
            # this is to separate plugin from core Cylc errors
            raise PluginError('cylc.pre_configure', entry_point.name,
                              exc) from None

    reinstall_workflow(
        named_run=named_run,
        rundir=run_dir,
        source=source,
        dry_run=False  # TODO: ready for dry run implementation
    )

    for entry_point in pkg_resources.iter_entry_points('cylc.post_install'):
        try:
            entry_point.resolve()(srcdir=source,
                                  opts=opts,
                                  rundir=str(run_dir))
        except Exception as exc:
            # NOTE: except Exception (purposefully vague)
            # this is to separate plugin from core Cylc errors
            raise PluginError('cylc.post_install', entry_point.name,
                              exc) from None
示例#5
0
def search_install_source_dirs(flow_name: str) -> Path:
    """Return the path of a workflow source dir if it is present in the
    'global.cylc[install]source dirs' search path."""
    search_path: List[str] = glbl_cfg().get(['install', 'source dirs'])
    if not search_path:
        raise WorkflowFilesError(
            "Cannot find workflow as 'global.cylc[install]source dirs' "
            "does not contain any paths")
    for path in search_path:
        try:
            flow_file = check_flow_file(Path(path, flow_name))
            return flow_file.parent
        except WorkflowFilesError:
            continue
    raise WorkflowFilesError(
        f"Could not find workflow '{flow_name}' in: {', '.join(search_path)}")
示例#6
0
def check_flow_file(
    path: Union[Path, str], symlink_suiterc: bool = False,
    logger: 'Logger' = LOG
) -> Path:
    """Raises WorkflowFilesError if no flow file in path sent.

    Args:
        path: Path to check for a flow.cylc and/or suite.rc file.
        symlink_suiterc: If True and suite.rc exists but not flow.cylc, create
            flow.cylc as a symlink to suite.rc.
        logger: A custom logger to use to log warnings.

    Returns the path of the flow file if present.
    """
    flow_file_path = Path(expand_path(path), SuiteFiles.FLOW_FILE)
    if flow_file_path.is_file():
        # Note: this includes if flow.cylc is a symlink
        return flow_file_path
    suite_rc_path = Path(path, SuiteFiles.SUITE_RC)
    if suite_rc_path.is_file():
        if symlink_suiterc:
            flow_file_path.symlink_to(suite_rc_path)
            logger.warning(
                f'The filename "{SuiteFiles.SUITE_RC}" is deprecated in '
                f'favour of "{SuiteFiles.FLOW_FILE}". Symlink created.')
        return suite_rc_path
    raise WorkflowFilesError(
        f"no {SuiteFiles.FLOW_FILE} or {SuiteFiles.SUITE_RC} in {path}")
示例#7
0
def make_localhost_symlinks(rund, named_sub_dir):
    """Creates symlinks for any configured symlink dirs from glbl_cfg.
    Args:
        rund: the entire run directory path
        named_sub_dir: e.g flow_name/run1

    Returns:
         dict - A dictionary of Symlinks with sources as keys and
         destinations as values: ``{source: destination}``

    """
    dirs_to_symlink = get_dirs_to_symlink(get_localhost_install_target(),
                                          named_sub_dir)
    symlinks_created = {}
    for key, value in dirs_to_symlink.items():
        if key == 'run':
            dst = rund
        else:
            dst = os.path.join(rund, key)
        src = expand_path(value)
        if '$' in src:
            raise WorkflowFilesError(
                f'Unable to create symlink to {src}.'
                f' \'{value}\' contains an invalid environment variable.'
                ' Please check configuration.')
        symlink_success = make_symlink(src, dst)
        # symlink info returned for logging purposes, symlinks created
        # before logs as this dir may be a symlink.
        if symlink_success:
            symlinks_created[src] = dst
    return symlinks_created
示例#8
0
def clean(reg):
    """Remove a stopped workflow from the local filesystem only.

    Deletes the workflow run directory and any symlink dirs. Note: if the
    run dir has already been manually deleted, it will not be possible to
    clean the symlink dirs.

    Args:
        reg (str): Workflow name.
    """
    run_dir = Path(get_workflow_run_dir(reg))
    try:
        _clean_check(reg, run_dir)
    except FileNotFoundError as exc:
        LOG.info(str(exc))
        return

    # Note: 'share/cycle' must come first, and '' must come last
    for possible_symlink in (
            SuiteFiles.SHARE_CYCLE_DIR, SuiteFiles.SHARE_DIR,
            SuiteFiles.LOG_DIR, SuiteFiles.WORK_DIR, ''):
        name = Path(possible_symlink)
        path = Path(run_dir, possible_symlink)
        if path.is_symlink():
            # Ensure symlink is pointing to expected directory. If not,
            # something is wrong and we should abort
            target = path.resolve()
            if target.exists() and not target.is_dir():
                raise WorkflowFilesError(
                    f'Invalid Cylc symlink directory {path} -> {target}\n'
                    f'Target is not a directory')
            expected_end = str(Path('cylc-run', reg, name))
            if not str(target).endswith(expected_end):
                raise WorkflowFilesError(
                    f'Invalid Cylc symlink directory {path} -> {target}\n'
                    f'Expected target to end with "{expected_end}"')
            # Remove <symlink_dir>/cylc-run/<reg>
            target_cylc_run_dir = str(target).rsplit(str(reg), 1)[0]
            target_reg_dir = Path(target_cylc_run_dir, reg)
            if target_reg_dir.is_dir():
                remove_dir(target_reg_dir)
            # Remove empty parents
            _remove_empty_reg_parents(reg, target_reg_dir)

    remove_dir(run_dir)
    _remove_empty_reg_parents(reg, run_dir)
示例#9
0
 def _check_child_dirs(path, depth_count=1):
     for result in os.scandir(path):
         if result.is_dir() and not result.is_symlink():
             if is_valid_run_dir(result.path):
                 raise WorkflowFilesError(
                     exc_msg %
                     (flow_name, result.path))
             if depth_count < MAX_SCAN_DEPTH:
                 _check_child_dirs(result.path, depth_count + 1)
示例#10
0
def get_run_dir(run_path_base, run_name, no_run_name):
    """ Build run directory for current install.

    Args:
        run_path_base (Path):
            The workflow directory.
        run_name (str):
            Name of the run.
        no_run_name (bool):
            Flag as True to indicate no run name - workflow installed into
            ~/cylc-run/<run_path_base>.

    Returns:
        relink (bool):
            True if runN symlink needs updating.
        run_num (int):
            Run number of the current install.
        rundir (Path):
            Run directory.
    """
    relink = False
    run_num = 0
    if no_run_name:
        rundir = run_path_base
    elif run_name:
        rundir = run_path_base.joinpath(run_name)
        if (run_path_base.exists() and
                detect_flow_exists(run_path_base, True)):
            raise WorkflowFilesError(
                f"This path: \"{run_path_base}\" contains installed numbered"
                " runs. Try again, using cylc install without --run-name.")
    else:
        run_n = Path(run_path_base, SuiteFiles.RUN_N).expanduser()
        run_num = get_next_rundir_number(run_path_base)
        rundir = Path(run_path_base, f'run{run_num}')
        if run_path_base.exists() and detect_flow_exists(run_path_base, False):
            raise WorkflowFilesError(
                f"This path: \"{run_path_base}\" contains an installed"
                " workflow. Try again, using --run-name.")
        unlink_runN(run_n)
        relink = True
    return relink, run_num, rundir
示例#11
0
def make_symlink(path: Union[Path, str], target: Union[Path, str]) -> bool:
    """Makes symlinks for directories.

    Args:
        path: Absolute path of the desired symlink.
        target: Absolute path of the symlink's target directory.
    """
    path = Path(path)
    target = Path(target)
    if path.exists():
        # note all three checks are needed here due to case where user has set
        # their own symlink which does not match the global config set one.
        if path.is_symlink() and target.exists() and path.samefile(target):
            # correct symlink already exists
            return False
        # symlink name is in use by a physical file or directory
        # log and return
        LOG.debug(
            f"Unable to create symlink to {target}. "
            f"The path {path} already exists.")
        return False
    elif path.is_symlink():
        # remove a bad symlink.
        try:
            path.unlink()
        except OSError:
            raise WorkflowFilesError(
                f"Error when symlinking. Failed to unlink bad symlink {path}.")
    target.mkdir(parents=True, exist_ok=True)

    # This is needed in case share and share/cycle have the same symlink dir:
    if path.exists():
        return False

    path.parent.mkdir(parents=True, exist_ok=True)
    try:
        path.symlink_to(target)
        return True
    except OSError as exc:
        raise WorkflowFilesError(f"Error when symlinking\n{exc}")
示例#12
0
def get_run_dir_info(
    run_path_base: Path, run_name: Optional[str], no_run_name: bool
) -> Tuple[bool, Optional[int], Path]:
    """Get (numbered, named or unnamed) run directory info for current install.

    Args:
        run_path_base: The workflow directory absolute path.
        run_name: Name of the run.
        no_run_name: Flag as True to indicate no run name - workflow installed
            into ~/cylc-run/<run_path_base>.

    Returns:
        relink: True if runN symlink needs updating.
        run_num: Run number of the current install, if using numbered runs.
        rundir: Run directory absolute path.
    """
    relink = False
    run_num = None
    if no_run_name:
        rundir = run_path_base
    elif run_name:
        rundir = run_path_base.joinpath(run_name)
        if (run_path_base.exists() and
                detect_flow_exists(run_path_base, True)):
            raise WorkflowFilesError(
                f"This path: \"{run_path_base}\" contains installed numbered"
                " runs. Try again, using cylc install without --run-name.")
    else:
        run_num = get_next_rundir_number(run_path_base)
        rundir = Path(run_path_base, f'run{run_num}')
        if run_path_base.exists() and detect_flow_exists(run_path_base, False):
            raise WorkflowFilesError(
                f"This path: \"{run_path_base}\" contains an installed"
                " workflow. Try again, using --run-name.")
        unlink_runN(run_path_base)
        relink = True
    return relink, run_num, rundir
示例#13
0
def validate_source_dir(source, flow_name):
    """Ensure the source directory is valid.

    Args:
        source (path): Path to source directory
    Raises:
        WorkflowFilesError:
            If log, share, work or _cylc-install directories exist in the
            source directory.
            Cylc installing from within the cylc-run dir
    """
    # Ensure source dir does not contain log, share, work, _cylc-install
    for dir_ in SuiteFiles.RESERVED_DIRNAMES:
        if Path(source, dir_).exists():
            raise WorkflowFilesError(
                f'{flow_name} installation failed. - {dir_} exists in source '
                'directory.')
    cylc_run_dir = Path(get_workflow_run_dir(''))
    if (os.path.abspath(os.path.realpath(cylc_run_dir))
            in os.path.abspath(os.path.realpath(source))):
        raise WorkflowFilesError(
            f'{flow_name} installation failed. Source directory should not be '
            f'in {cylc_run_dir}')
    check_flow_file(source)
示例#14
0
def _parse_src_path(id_):
    src_path = Path(id_)
    if (id_ == os.curdir or id_.startswith(f'{os.curdir}{os.sep}')
            or Path(id_).is_absolute()):
        src_path = src_path.resolve()
        if not src_path.exists():
            raise UserInputError(f'Path does not exist: {src_path}')
        if src_path.name in {WorkflowFiles.FLOW_FILE, WorkflowFiles.SUITE_RC}:
            src_path = src_path.parent
        try:
            src_file_path = check_flow_file(src_path)
        except WorkflowFilesError:
            raise WorkflowFilesError(NO_FLOW_FILE_MSG.format(id_))
        workflow_id = src_path.name
        return workflow_id, src_path, src_file_path
    return None
示例#15
0
def check_flow_file(
    path: Union[Path, str],
    symlink_suiterc: bool = False,
    logger: Optional['Logger'] = LOG
) -> Path:
    """Raises WorkflowFilesError if no flow file in path sent.

    Args:
        path: Path to check for a flow.cylc and/or suite.rc file.
        symlink_suiterc: If True and suite.rc exists, create flow.cylc as a
            symlink to suite.rc. If a flow.cylc symlink already exists but
            points elsewhere, it will be replaced.
        logger: A custom logger to use to log warnings.

    Returns the path of the flow file if present.
    """
    flow_file_path = Path(expand_path(path), WorkflowFiles.FLOW_FILE)
    suite_rc_path = Path(expand_path(path), WorkflowFiles.SUITE_RC)
    depr_msg = (
        f'The filename "{WorkflowFiles.SUITE_RC}" is deprecated '
        f'in favour of "{WorkflowFiles.FLOW_FILE}"')
    if flow_file_path.is_file():
        if not flow_file_path.is_symlink():
            return flow_file_path
        if flow_file_path.resolve() == suite_rc_path.resolve():
            # A symlink that points to *existing* suite.rc
            if logger:
                logger.warning(depr_msg)
            return flow_file_path
    if suite_rc_path.is_file():
        if not symlink_suiterc:
            if logger:
                logger.warning(depr_msg)
            return suite_rc_path
        if flow_file_path.is_symlink():
            # Symlink broken or points elsewhere - replace
            flow_file_path.unlink()
        flow_file_path.symlink_to(WorkflowFiles.SUITE_RC)
        if logger:
            logger.warning(f'{depr_msg}. Symlink created.')
        return flow_file_path
    raise WorkflowFilesError(
        f"no {WorkflowFiles.FLOW_FILE} or {WorkflowFiles.SUITE_RC} in {path}")
示例#16
0
def _clean_check(reg, run_dir):
    """Check whether a workflow can be cleaned.

    Args:
        reg (str): Workflow name.
        run_dir (str): Path to the workflow run dir on the filesystem.
    """
    validate_flow_name(reg)
    reg = os.path.normpath(reg)
    if reg.startswith('.'):
        raise WorkflowFilesError(
            "Workflow name cannot be a path that points to the cylc-run "
            "directory or above")
    if not run_dir.is_dir() and not run_dir.is_symlink():
        msg = f"No directory to clean at {run_dir}"
        raise FileNotFoundError(msg)
    try:
        detect_old_contact_file(reg)
    except SuiteServiceFileError as exc:
        raise SuiteServiceFileError(
            f"Cannot remove running workflow.\n\n{exc}")
示例#17
0
def make_localhost_symlinks(
    rund: Union[Path, str],
    named_sub_dir: str,
    symlink_conf: Optional[Dict[str, Dict[str, str]]] = None
) -> Dict[str, Union[Path, str]]:
    """Creates symlinks for any configured symlink dirs from glbl_cfg.
    Args:
        rund: the entire run directory path
        named_sub_dir: e.g workflow_name/run1
        symlink_conf: Symlinks dirs configuration passed from cli

    Returns:
        Dictionary of symlinks with sources as keys and
        destinations as values: ``{source: destination}``

    """
    symlinks_created = {}
    dirs_to_symlink = get_dirs_to_symlink(
        get_localhost_install_target(),
        named_sub_dir, symlink_conf=symlink_conf
    )
    for key, value in dirs_to_symlink.items():
        if value is None:
            continue
        if key == 'run':
            symlink_path = rund
        else:
            symlink_path = os.path.join(rund, key)
        target = expand_path(value)
        if '$' in target:
            raise WorkflowFilesError(
                f'Unable to create symlink to {target}.'
                f' \'{value}\' contains an invalid environment variable.'
                ' Please check configuration.')
        symlink_success = make_symlink(symlink_path, target)
        # Symlink info returned for logging purposes. Symlinks should be
        # created before logs as the log dir may be a symlink.
        if symlink_success:
            symlinks_created[target] = symlink_path
    return symlinks_created
示例#18
0
def check_nested_run_dirs(run_dir: Union[Path, str], flow_name: str) -> None:
    """Disallow nested run dirs e.g. trying to install foo/bar where foo is
    already a valid workflow directory.

    Args:
        run_dir: Absolute workflow run directory path.
        flow_name: Workflow name.

    Raise:
        WorkflowFilesError:
            - reg dir is nested inside a run dir
            - reg dir contains a nested run dir (if not deeper than max scan
                depth)
    """
    exc_msg = (
        'Nested run directories not allowed - cannot install workflow name '
        '"{0}" as "{1}" is already a valid run directory.')

    def _check_child_dirs(path: Union[Path, str], depth_count: int = 1):
        for result in os.scandir(path):
            if result.is_dir() and not result.is_symlink():
                if is_valid_run_dir(result.path):
                    raise WorkflowFilesError(
                        exc_msg.format(flow_name, result.path)
                    )
                if depth_count < MAX_SCAN_DEPTH:
                    _check_child_dirs(result.path, depth_count + 1)

    reg_path: Union[Path, str] = os.path.normpath(run_dir)
    parent_dir = os.path.dirname(reg_path)
    while parent_dir not in ['', '/']:
        if is_valid_run_dir(parent_dir):
            raise WorkflowFilesError(
                exc_msg.format(parent_dir, get_cylc_run_abs_path(parent_dir))
            )
        parent_dir = os.path.dirname(parent_dir)

    reg_path = get_cylc_run_abs_path(reg_path)
    if os.path.isdir(reg_path):
        _check_child_dirs(reg_path)
示例#19
0
def install_workflow(
    flow_name: Optional[str] = None,
    source: Optional[Union[Path, str]] = None,
    run_name: Optional[str] = None,
    no_run_name: bool = False,
    no_symlinks: bool = False
) -> Tuple[Path, Path, str]:
    """Install a workflow, or renew its installation.

    Install workflow into new run directory.
    Create symlink to suite source location, creating any symlinks for run,
    work, log, share, share/cycle directories.

    Args:
        flow_name: workflow name, default basename($PWD).
        source: directory location of flow.cylc file, default $PWD.
        run_name: name of the run, overrides run1, run2, run 3 etc...
            If specified, cylc install will not create runN symlink.
        rundir: for overriding the default cylc-run directory.
        no_run_name: Flag as True to install workflow into
            ~/cylc-run/<flow_name>
        no_symlinks: Flag as True to skip making localhost symlink dirs

    Return:
        source: The source directory.
        rundir: The directory the workflow has been installed into.
        flow_name: The installed suite name (which may be computed here).

    Raise:
        WorkflowFilesError:
            No flow.cylc file found in source location.
            Illegal name (can look like a relative path, but not absolute).
            Another suite already has this name (unless --redirect).
            Trying to install a workflow that is nested inside of another.
    """

    if not source:
        source = Path.cwd()
    elif Path(source).name == SuiteFiles.FLOW_FILE:
        source = Path(source).parent
    source = Path(expand_path(source))
    if not flow_name:
        flow_name = Path.cwd().stem
    validate_flow_name(flow_name)
    if run_name in SuiteFiles.RESERVED_NAMES:
        raise WorkflowFilesError(
            f'Run name cannot be "{run_name}".')
    validate_source_dir(source, flow_name)
    run_path_base = Path(get_workflow_run_dir(flow_name))
    relink, run_num, rundir = get_run_dir(run_path_base, run_name, no_run_name)
    if Path(rundir).exists():
        raise WorkflowFilesError(
            f"\"{rundir}\" exists."
            " Try using cylc reinstall. Alternatively, install with another"
            " name, using the --run-name option.")
    check_nested_run_dirs(rundir, flow_name)
    symlinks_created = {}
    if not no_symlinks:
        sub_dir = flow_name
        if run_num:
            sub_dir += '/' + f'run{run_num}'
        symlinks_created = make_localhost_symlinks(rundir, sub_dir)
    INSTALL_LOG = _get_logger(rundir, 'cylc-install')
    if not no_symlinks and bool(symlinks_created) is True:
        for src, dst in symlinks_created.items():
            INSTALL_LOG.info(f"Symlink created from {src} to {dst}")
    try:
        rundir.mkdir(exist_ok=True)
    except OSError as e:
        if e.strerror == "File exists":
            raise WorkflowFilesError(f"Run directory already exists : {e}")
    if relink:
        link_runN(rundir)
    create_workflow_srv_dir(rundir)
    rsync_cmd = get_rsync_rund_cmd(source, rundir)
    proc = Popen(rsync_cmd, stdout=PIPE, stderr=PIPE, text=True)
    stdout, stderr = proc.communicate()
    INSTALL_LOG.info(f"Copying files from {source} to {rundir}")
    INSTALL_LOG.info(f"{stdout}")
    if not proc.returncode == 0:
        INSTALL_LOG.warning(
            f"An error occurred when copying files from {source} to {rundir}")
        INSTALL_LOG.warning(f" Error: {stderr}")
    cylc_install = Path(rundir.parent, SuiteFiles.Install.DIRNAME)
    check_flow_file(rundir, symlink_suiterc=True, logger=INSTALL_LOG)
    if no_run_name:
        cylc_install = Path(rundir, SuiteFiles.Install.DIRNAME)
    source_link = cylc_install.joinpath(SuiteFiles.Install.SOURCE)
    cylc_install.mkdir(parents=True, exist_ok=True)
    if not source_link.exists():
        INSTALL_LOG.info(f"Creating symlink from {source_link}")
        source_link.symlink_to(source)
    elif source_link.exists() and (os.readlink(source_link) == str(source)):
        INSTALL_LOG.info(
            f"Symlink from \"{source_link}\" to \"{source}\" in place.")
    else:
        raise WorkflowFilesError(
            "Source directory between runs are not consistent.")
    # check source link matches the source symlink from workflow dir.
    INSTALL_LOG.info(f'INSTALLED {flow_name} from {source} -> {rundir}')
    print(f'INSTALLED {flow_name} from {source} -> {rundir}')
    _close_install_log(INSTALL_LOG)
    return source, rundir, flow_name