Exemplo n.º 1
0
    def __init__(self, username, password, repository_url=None, test=False):

        if not username:
            raise exceptions.InvalidArgumentsException(
                'username cannot be None')

        if not password:
            raise exceptions.InvalidArgumentsException(
                'password cannot be None')

        if repository_url and test:
            raise exceptions.InvalidArgumentsException(
                'either repository_url or test is allowed')

        self.test = test
        self.repository_url = 'https://test.pypi.org/legacy/' if self.test else repository_url
        self.username = username
        self.password = password
        self._runner = LocalCommandRunner()
        self._site = 'test.pypi.org' if self.test else 'pypi.org'
        self._logger = logger.Logger(__name__)
        self._log_ctx = {
            'test': self.test,
            'repository_url': self.repository_url,
            'site': self._site
        }
Exemplo n.º 2
0
    def __init__(self, sha, current_version):

        if not sha:
            raise exceptions.InvalidArgumentsException('sha cannot be empty')

        if not current_version:
            raise exceptions.InvalidArgumentsException(
                'current_version cannot be empty')

        try:
            semver.parse(current_version)
        except (TypeError, ValueError):
            raise exceptions.InvalidArgumentsException(
                'Version is not a legal semantic '
                'version string')

        self._current_version = current_version
        self._sha = sha
        self._logger = logger.Logger(__name__)
        self._log_ctx = {
            'sha': self.sha,
            'current_version': self._current_version
        }

        self.features = set()
        self.bugs = set()
        self.issues = set()
        self.commits = set()
Exemplo n.º 3
0
    def create_branch(self, name, sha):
        """
        Create a branch.

        Args:
            name (str): The branch name.
            sha (str): The sha to create the branch from.

        Raises:
            exceptions.ShaNotFoundException: Raised if the given sha does not exist.
            exceptions.BranchAlreadyExistsException: Raised if the given branch name already exists.
        """

        if not name:
            raise exceptions.InvalidArgumentsException('name cannot be empty')

        if not sha:
            raise exceptions.InvalidArgumentsException('sha cannot be empty')

        try:

            self._debug('Creating branch...', name=name, sha=sha)
            ref = self.repo.create_git_ref(ref='refs/heads/{}'.format(name),
                                           sha=sha)
            self._debug('Created branch...', name=name, sha=sha)

            return model.Branch(impl=ref, sha=ref.object.sha, name=name)

        except GithubException as e:
            if e.data['message'] == 'Object does not exist':
                raise exceptions.CommitNotFoundException(sha=sha)
            if e.data['message'] == 'Reference already exists':
                raise exceptions.BranchAlreadyExistsException(
                    repo=self._repo_name, branch=name)
            raise  # pragma: no cover
Exemplo n.º 4
0
    def set_version(self, value, branch=None):
        """
        Sets the version of setup.py file to the specified version number.
        Note, This will actually create a commit on top of the branch you specify.

        Args:
            value (str): The semantic version string.
            branch (:str, optional): The branch to push to commit to. Defaults to the default repository branch.

        Returns:
            pyci.api.model.Bump: The commit that was created.

        Raises:
            pyci.api.exceptions.TargetVersionEqualsCurrentVersionException:
                Current version in setup.py is equal to the given value.

        """

        if not value:
            raise exceptions.InvalidArgumentsException('value cannot be empty')

        try:
            semver.parse(value)
        except (TypeError, ValueError):
            raise exceptions.InvalidArgumentsException(
                'value is not a legal semantic version')

        branch = branch or self.default_branch_name

        last_branch_commit = self._get_or_create_commit(sha=branch)

        current_version = last_branch_commit.setup_py_version

        self._debug('Generating setup.py file contents...', next_version=value)
        setup_py = utils.generate_setup_py(last_branch_commit.setup_py, value)
        self._debug('Generated setup.py file contents.')

        commit_message = BUMP_VERSION_COMMIT_MESSAGE_FORMAT.format(value)

        if current_version == value:
            raise exceptions.TargetVersionEqualsCurrentVersionException(
                version=current_version)

        bump_commit = self.commit(path='setup.py',
                                  contents=setup_py,
                                  message=commit_message)
        return model.Bump(impl=bump_commit.impl,
                          prev_version=current_version,
                          next_version=value,
                          sha=bump_commit.sha)
Exemplo n.º 5
0
    def __init__(self, title, url, timestamp):

        if not title:
            raise exceptions.InvalidArgumentsException('title cannot be empty')

        if not url:
            raise exceptions.InvalidArgumentsException('url cannot be empty')

        if not timestamp:
            raise exceptions.InvalidArgumentsException(
                'timestamp cannot be empty')

        self.timestamp = timestamp
        self.title = title
        self.url = url
Exemplo n.º 6
0
    def commit(self, path, contents, message, branch=None):
        """
        Commit a file to the repository.

        Args:
            path (str): Path to the file, relative to the repository root.
            contents (str): The new contents of the file.
            message (str): The commit message.
            branch (:str, optional): The branch to commit to. Defaults to the repository default branch.
        """

        if not path:
            raise exceptions.InvalidArgumentsException('path cannot be empty')

        if not contents:
            raise exceptions.InvalidArgumentsException(
                'contents cannot be empty')

        if not message:
            raise exceptions.InvalidArgumentsException(
                'message cannot be empty')

        branch = branch or self.default_branch_name

        self._debug('Fetching last commit for branch...')
        try:
            last_commit = self.repo.get_commit(sha=branch)
        except GithubException as e:
            if isinstance(e, UnknownObjectException
                          ) or 'No commit found for SHA' in str(e):
                raise exceptions.BranchNotFoundException(
                    branch=branch, repo=self.repo.full_name)
            raise  # pragma: no cover
        self._debug('Fetched last commit for branch.',
                    last_commit=last_commit.sha)

        commit = self._create_commit(path, contents, message, last_commit.sha)

        self._debug('Updating branch to point to commit...',
                    branch=branch,
                    sha=commit.sha)
        ref = self.repo.get_git_ref(ref='heads/{0}'.format(branch))
        ref.edit(sha=commit.sha)
        self._debug('Updated branch to point to commit',
                    branch=branch,
                    sha=ref.object.sha)

        return commit
Exemplo n.º 7
0
    def __init__(self, repo, access_token):

        if not repo:
            raise exceptions.InvalidArgumentsException('repo cannot be empty')

        if not access_token:
            raise exceptions.InvalidArgumentsException(
                'access_token cannot be empty')

        self.__commits = {}
        self._logger = logger.Logger(__name__)
        self._hub = Github(access_token, timeout=30)
        self._repo_name = repo
        self._log_ctx = {
            'repo': self._repo_name,
        }
Exemplo n.º 8
0
    def delete_tag(self, name):
        """
        Delete a tag.

        Args:
            name (str): The tag name.

        Raises:
            TagNotFoundException: Raised when the given tag is not found.
        """

        if not name:
            raise exceptions.InvalidArgumentsException('name cannot be empty')

        try:
            self._debug('Fetching tag...', name=name)
            tag = self.repo.get_git_ref('tags/{0}'.format(name))
            self._debug('Fetched tag.', ref=tag.ref)

            self._debug('Deleting tag...', ref=tag.ref)
            tag.delete()
            self._debug('Deleted tag.', ref=tag.ref)

        except UnknownObjectException:
            raise exceptions.TagNotFoundException(tag=name)
Exemplo n.º 9
0
    def delete_release(self, name):
        """
        Delete a release. Note that this does not delete the tag associated with this release.
        To delete the tag, use the 'delete_tag' method.

        Args:
            name (str): The release name.

        Raises:
            ReleaseNotFoundException: Raised when the given release is not found.
        """

        if not name:
            raise exceptions.InvalidArgumentsException('name cannot be empty')

        try:

            self._debug('Fetching release...', name=name)
            rel = self.repo.get_release(id=name)
            self._debug('Fetched release.', name=rel.title)

            self._debug('Deleting release...', name=rel.title)
            rel.delete_release()
            self._debug('Deleted release.', name=rel.title)

        except UnknownObjectException:
            raise exceptions.ReleaseNotFoundException(release=name)
Exemplo n.º 10
0
    def delete_branch(self, name):
        """
        Delete a branch.

        Args:
            name (str): The branch name.

        Raises: exceptions.BranchNotFoundException: Raised when the branch with the given name
            does not exist.
        """

        if not name:
            raise exceptions.InvalidArgumentsException('name cannot be empty')

        try:

            self._debug('Fetching branch...', name=name)
            ref = self.repo.get_git_ref(ref='heads/{}'.format(name))
            self._debug('Fetched branch...', name=name)

            self._debug('Deleting reference...', ref=ref.ref)
            ref.delete()
            self._debug('Deleted reference.', ref=ref.ref)

        except UnknownObjectException:
            raise exceptions.BranchNotFoundException(branch=name,
                                                     repo=self._repo_name)
Exemplo n.º 11
0
    def add(self, change):
        """
        Add a change to this changelog. Based on the kind_modifier of the change, this will add it
        to the appropriate collection, which can later be retrieved.

        Args:
            change (:ChangelogIssue:ChangelogCommit): Either a commit or an issue.
        """

        if not isinstance(change, (ChangelogCommit, ChangelogIssue)):
            raise exceptions.InvalidArgumentsException(
                'change must be of type '
                '`pyci.api.changelog.ChangelogCommit` or '
                '`pyci.api.changelog.ChangelogIssue`')

        if isinstance(change, ChangelogIssue):
            if change.kind_modifier == ChangelogIssue.FEATURE:
                self.features.add(change)
            if change.kind_modifier == ChangelogIssue.BUG:
                self.bugs.add(change)
            if change.kind_modifier == ChangelogIssue.ISSUE:
                self.issues.add(change)

        if isinstance(change, ChangelogCommit):
            self.commits.add(change)
Exemplo n.º 12
0
    def upload_asset(self, asset, release):
        """
        Upload an asset to an existing Github release.

        Args:
            asset (str): Path to the asset file.
            release (str): The release name.

        Returns:
            str: The uploaded asset URL.

        Raises:
            AssetAlreadyPublishedException: Raised when the given asset already exists in the
                release. This is determined by the basename of the asset file path.

        """

        if not asset:
            raise exceptions.InvalidArgumentsException('asset cannot be empty')

        if not release:
            raise exceptions.InvalidArgumentsException(
                'release cannot be empty')

        asset = os.path.abspath(asset)

        utils.validate_file_exists(asset)

        self._debug('Fetching release...', name=release)
        git_release = self.get_release(title=release).impl
        self._debug('Fetched release.', url=git_release.html_url)

        try:
            self._debug('Uploading asset...', asset=asset, release=release)
            git_release.upload_asset(path=asset,
                                     content_type='application/octet-stream')
            asset_url = 'https://github.com/{0}/releases/download/{1}/{2}'.format(
                self._repo_name, git_release.title, os.path.basename(asset))
            self._debug('Uploaded asset.', url=asset_url, release=release)
            return asset_url
        except GithubException as e:

            if e.data['errors'][0]['code'] == 'already_exists':
                asset_name = os.path.basename(asset)
                raise exceptions.AssetAlreadyPublishedException(
                    asset=asset_name, release=git_release.title)
            raise  # pragma: no cover
Exemplo n.º 13
0
    def bump_version(self, semantic, branch=None):
        """
        Bump the version of setup.py according to the semantic specification.
        The base version is retrieved from the current setup.py file of the branch.
        Note, This will actually create a commit on top of the branch you specify.

        Args:
            semantic (:str, optional): A semantic version modifier (e.g minor, major..).
            branch (:str, optional): The branch to push to commit to. Defaults to the default repository branch.

        Returns:
            pyci.api.model.Commit: The commit that was created.

        Raises:
            pyci.api.exceptions.TargetVersionEqualsCurrentVersionException:
                Current version in setup.py is equal to the calculated version number.
        """

        if not semantic:
            raise exceptions.InvalidArgumentsException(
                'semantic cannot be empty')

        if semantic not in model.ChangelogIssue.SEMANTIC_VERSION_LABELS:
            raise exceptions.InvalidArgumentsException(
                'semantic must be one of: {}'.format(
                    model.ChangelogIssue.SEMANTIC_VERSION_LABELS))

        branch = branch or self.default_branch_name

        last_branch_commit = self._get_or_create_commit(sha=branch)

        current_version = last_branch_commit.setup_py_version

        bumps = {
            model.ChangelogIssue.PATCH: semver.bump_patch,
            model.ChangelogIssue.MINOR: semver.bump_minor,
            model.ChangelogIssue.MAJOR: semver.bump_major
        }

        self._debug('Determining next version',
                    current_version=current_version)
        next_version = bumps[semantic](current_version)
        self._debug('Determined next version',
                    current_version=current_version,
                    next_version=next_version)

        return self.set_version(value=next_version, branch=branch)
Exemplo n.º 14
0
    def close_issue(self, num, release):
        """
        Close an issue as part of a specific release. This method will add a comment to the
        issue, specifying the release its a part of.

        Args:
            num (int): The issue number.
            release (str): The release title this issue if a part of.

        Raises:
            ReleaseNotFoundException: Raised when the given release title does not exist.
        """

        if not num:
            raise exceptions.InvalidArgumentsException('num cannot be empty')

        if not release:
            raise exceptions.InvalidArgumentsException(
                'release cannot be empty')

        try:
            issue = self.repo.get_issue(number=num)
        except UnknownObjectException:
            raise exceptions.IssueNotFoundException(issue=num)

        try:
            git_release = self.repo.get_release(id=release)
        except UnknownObjectException:
            raise exceptions.ReleaseNotFoundException(release=release)

        self._debug('Closing issue...', issue=issue.number)
        issue.edit(state='closed')
        self._debug('Closed issue.', issue=issue.number)

        issue_comments = [comment.body for comment in issue.get_comments()]

        issue_comment = 'This issue is part of release [{}]({})'.format(
            git_release.title, git_release.html_url)

        if issue_comment not in issue_comments:
            self._debug('Adding a comment to issue...', issue=issue.number)
            issue.create_comment(body=issue_comment)
            self._debug('Added comment.',
                        issue=issue.number,
                        comment=issue_comment)
Exemplo n.º 15
0
    def reset_branch(self, name, sha, hard=False):
        """
        Reset the branch to the sha. This is equivalent to 'git reset'.

        Args:
            name (:str): The branch name.
            sha (:str): The sha to reset to.
            hard (:bool, optional): Preform a hard reset. Defaults to false.
        """

        if not name:
            raise exceptions.InvalidArgumentsException('name cannot be empty')

        if not sha:
            raise exceptions.InvalidArgumentsException('sha cannot be empty')

        try:
            self._reset_ref(ref='heads/{}'.format(name), sha=sha, hard=hard)
        except UnknownObjectException:
            raise exceptions.BranchNotFoundException(branch=name,
                                                     repo=self._repo_name)
Exemplo n.º 16
0
    def __init__(self,
                 repo=None,
                 sha=None,
                 path=None,
                 target_dir=None,
                 python=None):

        if sha and not repo:
            raise exceptions.InvalidArgumentsException(
                'Must pass repo as well when passing sha')

        if sha and path:
            raise exceptions.InvalidArgumentsException(
                "Either 'sha' or 'path' is allowed")

        if repo and path:
            raise exceptions.InvalidArgumentsException(
                "Either 'repo' or 'path' is allowed")

        if not sha and not path:
            raise exceptions.InvalidArgumentsException(
                "Either 'sha' or 'path' is required")

        if target_dir:
            utils.validate_directory_exists(target_dir)

        if path:
            utils.validate_directory_exists(path)

        self._repo_location = path if path else '{}@{}'.format(repo, sha)
        self._python = python
        self._target_dir = target_dir or os.getcwd()
        self._logger = logger.Logger(__name__)
        self._runner = LocalCommandRunner(log=self._logger)
        self._log_ctx = {
            'repo': self._repo_location,
        }
        self._repo_dir = self._create_repo(path, sha, repo)
Exemplo n.º 17
0
    def upload_changelog(self, changelog, release):
        """
        Upload a changelog to the release.

        Note this will override the existing (if any) changelog.

        Args:
            changelog (str): The changelog string.
            release (str): The release name.
        """

        if not changelog:
            raise exceptions.InvalidArgumentsException(
                'changelog cannot be empty')

        if not release:
            raise exceptions.InvalidArgumentsException(
                'release cannot be empty')

        try:
            self._debug('Fetching release...', name=release)
            git_release = self.repo.get_release(id=release)
            self._debug('Fetched release.', url=git_release.html_url)
        except UnknownObjectException:
            raise exceptions.ReleaseNotFoundException(release=release)

        self._logger.debug('Updating release with changelog...',
                           release=release)
        git_release.update_release(name=git_release.title, message=changelog)
        self._logger.debug('Successfully updated release with changelog',
                           release=release)

        release_sha = self.repo.get_git_ref(
            ref='tags/{}'.format(git_release.tag_name)).object.sha
        return model.Release(impl=git_release,
                             title=git_release.title,
                             url=git_release.html_url,
                             sha=release_sha)
Exemplo n.º 18
0
    def __init__(self, name, level=None, ch=None, fmt=None):

        if not name:
            raise exceptions.InvalidArgumentsException('name cannot be empty')

        level = level or DEFAULT_LOG_LEVEL
        fmt = fmt or DEFAULT_LOG_FORMAT

        self._name = name
        self._logger = logging.getLogger(name)
        self._logger.propagate = False

        if not self._logger.handlers:
            self.add_console_handler(level, ch, fmt)
        self.set_level(level)
Exemplo n.º 19
0
    def get_release(self, title):
        """
        Fetch a release by its title.

        Args:
            title: The release title.

        Returns:
            pyci.api.model.Release: The release object.

        """

        if not title:
            raise exceptions.InvalidArgumentsException('title cannot be empty')

        draft = False

        try:
            release = self.repo.get_release(id=title)
        except UnknownObjectException:

            # This might be a draft release, in which case we need to list and filter
            # since 'get' doesn't fetch draft releases. (but list does for some reason)
            releases = [
                r for r in self.repo.get_releases() if r.title == title
            ]

            if not releases:
                raise exceptions.ReleaseNotFoundException(release=title)

            release = releases[0]

            draft = True

        try:
            sha = self.repo.get_git_ref('tags/{}'.format(
                release.tag_name)).object.sha
        except UnknownObjectException:
            if draft:
                sha = None
            else:
                raise

        return model.Release(impl=release,
                             title=release.title,
                             url=release.html_url,
                             sha=sha)
Exemplo n.º 20
0
    def detect_issues(self, sha=None, message=None):
        """

        Detect which issues a commit is related to. This is done by using the following heuristic:

            1. Extract all links in the commit message. A link is defined as a number prefix by the '#' sign.

            2. For each link:

                2.1 - Check if it points to an issue.
                2.2 - Yes --> we are done.
                2.3 - No --> check if it points to a PR.
                2.4 - Yes --> extract links from the PR body and run 2.1 --> 2.2 --> 2.5 for each link.
                2.5 - No --> ignore this link and move on.

            3. Return all collected issues.

        Args:

            message (:str, optional): The commit message.

            sha (:str, optional): The commit sha.

        Return:

            list(pyci.api.model.Issue): All the collected issues.

        """

        if not sha and not message:
            raise exceptions.InvalidArgumentsException(
                'either sha or message is required.')

        if sha and message:
            raise exceptions.InvalidArgumentsException(
                'either sha or message is allowed.')

        def _fetch_message():
            try:
                self._debug('Fetching commit message...', sha=sha)
                commit_message = self.repo.get_commit(sha=sha).commit.message
                self._debug('Fetched commit message...',
                            sha=sha,
                            commit_message=commit_message)
                return commit_message
            except GithubException as e:
                if isinstance(e, UnknownObjectException
                              ) or 'No commit found for SHA' in str(e):
                    raise exceptions.CommitNotFoundException(sha=sha)
                else:
                    raise

        message = message or _fetch_message()

        self._debug('Detecting issues for commit...',
                    sha=sha,
                    commit_message=message)

        self._debug('Extracting commit links...',
                    sha=sha,
                    commit_message=message)
        commit_links = utils.extract_links(message)
        self._debug('Extracted commit links.',
                    sha=sha,
                    commit_message=message,
                    links=commit_links)

        issues = []

        for c_link in commit_links:

            try:

                self._debug('Fetching pull request...',
                            sha=sha,
                            commit_message=message,
                            ref=c_link)
                pull = self.repo.get_pull(number=c_link)
                self._debug('Fetched pull request.',
                            sha=sha,
                            commit_message=message,
                            ref=c_link,
                            pr=pull.url)

                self._debug('Extracting pull request links...',
                            sha=sha,
                            ref=c_link,
                            pr_body=pull.body)
                pr_links = utils.extract_links(pull.body)
                self._debug('Extracted pull request links.',
                            sha=sha,
                            ref=c_link,
                            pr_body=pull.body,
                            links=pr_links)

                for p_link in pr_links:

                    try:

                        self._debug('Fetching issue...',
                                    sha=sha,
                                    pr_body=pull.body,
                                    ref=p_link)
                        issue = self.repo.get_issue(number=p_link)
                        self._debug('Fetched issue.',
                                    sha=sha,
                                    pr_body=pull.body,
                                    issue_url=issue.url)

                        issues.append(
                            model.Issue(impl=issue,
                                        number=issue.number,
                                        url=issue.html_url))

                    except UnknownObjectException:
                        # ignore - it might not be a reference at all...
                        pass

            except UnknownObjectException:

                self._debug('Link is not a pull request.',
                            sha=sha,
                            commit_message=message,
                            ref=c_link)

                try:

                    self._debug('Fetching issue...',
                                sha=sha,
                                commit_message=message,
                                ref=c_link)
                    issue = self.repo.get_issue(number=c_link)
                    self._debug('Fetched issue.',
                                sha=sha,
                                commit_message=message,
                                issue_url=issue.url)

                    issues.append(
                        model.Issue(impl=issue,
                                    number=issue.number,
                                    url=issue.html_url))

                except UnknownObjectException:
                    # ignore - it might not be a reference at all...
                    pass

        return issues
Exemplo n.º 21
0
    def nsis(self,
             binary_path,
             version=None,
             output=None,
             author=None,
             url=None,
             copyright_string=None,
             description=None,
             license_path=None,
             program_files_dir=None):
        """
        Create a windows installer package.

        This method will produce an executable installer (.exe) that, when executed, will install
        the provided binary into "Program Files". In addition, it will manipulate the system PATH
        variable on the target machine so that the binary can be executed from any directory.

        Under the hood, this uses the NSIS project.

        For more information please visit https://nsis.sourceforge.io/Main_Page

        Args:

            binary_path (:str): True if the created will should be universal, False otherwise.

            version (:str, optional): Version string metadata. Defaults to the 'version' argument
                in your setup.py file.

            output (:str, optional): Target file to create. Defaults to
                {binary-path-basename}-installer.exe

            author (:str, optional): Package author. Defaults to the value specified in setup.py.

            url (:str, optional): URL to the package website. Defaults to the value specified in setup.py.

            copyright_string (:str, optional): Copyright. Defaults to an empty string.

            description (:str, optional): Package description. Defaults to the value specified in setup.py.

            license_path (:str, optional): Path to a license file. Defaults to the value specified in setup.py.

            program_files_dir (:str, optional): Directory name inside Program Files where the app will be installed.

        Raises:

            LicenseNotFoundException: License file doesn't exist.

            BinaryFileDoesntExistException: The provided binary file doesn't exist.

            FileExistsException: Destination file already exists.

            DirectoryDoesntExistException: The destination directory does not exist.

        """

        if not utils.is_windows():
            raise exceptions.WrongPlatformException(expected='Windows')

        if not binary_path:
            raise exceptions.InvalidArgumentsException('Must pass binary_path')

        try:
            self._debug('Validating binary exists: {}'.format(binary_path))
            utils.validate_file_exists(binary_path)
        except (exceptions.FileDoesntExistException,
                exceptions.FileIsADirectoryException):
            raise exceptions.BinaryDoesntExistException(binary_path)

        try:
            version = version or self._version
            self._debug('Validating version string: {}'.format(version))
            utils.validate_nsis_version(version)
        except exceptions.InvalidNSISVersionException as err:
            tb = sys.exc_info()[2]
            try:
                # Auto-correction attempt for standard python versions
                version = '{}.0'.format(version)
                utils.validate_nsis_version(version)
            except exceptions.InvalidNSISVersionException:
                utils.raise_with_traceback(err, tb)

        installer_base_name = os.path.basename(binary_path).replace('.exe', '')
        try:
            name = self._name
        except BaseException as e:
            self._debug(
                'Unable to extract default name from setup.py: {}. Using binary base name...'
                .format(str(e)))
            name = installer_base_name

        installer_name = '{}-installer'.format(installer_base_name)
        copyright_string = copyright_string or ''

        destination = os.path.abspath(
            output
            or '{}.exe'.format(os.path.join(self._target_dir, installer_name)))
        self._debug('Validating destination file does not exist: {}'.format(
            destination))
        utils.validate_file_does_not_exist(destination)

        target_directory = os.path.abspath(os.path.join(
            destination, os.pardir))

        self._debug(
            'Validating target directory exists: {}'.format(target_directory))
        utils.validate_directory_exists(target_directory)

        try:
            license_path = license_path or os.path.abspath(
                os.path.join(self._repo_dir, self._license))
            self._debug(
                'Validating license file exists: {}'.format(license_path))
            utils.validate_file_exists(license_path)
        except (exceptions.FileDoesntExistException,
                exceptions.FileIsADirectoryException) as e:
            raise exceptions.LicenseNotFoundException(str(e))

        author = author or self._author
        url = url or self._url
        description = description or self._description

        program_files_dir = program_files_dir or name

        config = {
            'name': name,
            'author': author,
            'website': url,
            'copyright': copyright_string,
            'license_path': license_path,
            'binary_path': binary_path,
            'description': description,
            'installer_name': installer_name,
            'program_files_dir': program_files_dir
        }

        temp_dir = tempfile.mkdtemp()

        try:

            support = 'windows_support'

            template = get_text_resource(
                os.path.join(support, 'installer.nsi.jinja'))
            nsis_zip_resource = get_binary_resource(
                os.path.join(support, 'nsis-3.04.zip'))
            path_header_resource = get_text_resource(
                os.path.join(support, 'path.nsh'))

            self._debug('Rendering nsi template...')
            nsi = Template(template).render(**config)
            installer_path = os.path.join(temp_dir, 'installer.nsi')
            with open(installer_path, 'w') as f:
                f.write(nsi)
            self._debug(
                'Finished rendering nsi template: {}'.format(installer_path))

            self._debug('Writing path header file...')
            path_header_path = os.path.join(temp_dir, 'path.nsh')
            with open(path_header_path, 'w') as header:
                header.write(path_header_resource)
            self._debug('Finished writing path header file: {}'.format(
                path_header_path))

            self._debug('Extracting NSIS from resources...')

            nsis_archive = os.path.join(temp_dir, 'nsis.zip')
            with open(nsis_archive, 'wb') as _w:
                _w.write(nsis_zip_resource)
            utils.unzip(nsis_archive, target_dir=temp_dir)
            self._debug(
                'Finished extracting makensis.exe from resources: {}'.format(
                    nsis_archive))

            makensis_path = os.path.join(temp_dir, 'nsis-3.04', 'makensis.exe')
            command = '{} -DVERSION={} {}'.format(makensis_path, version,
                                                  installer_path)

            # The installer expects the binary to be located in the working directory
            # and be named {{ name }}.exe.
            # See installer.nsi.jinja#L85
            expected_binary_path = os.path.join(temp_dir,
                                                '{}.exe'.format(name))
            self._debug('Copying binary to expected location: {}'.format(
                expected_binary_path))
            shutil.copyfile(src=binary_path, dst=expected_binary_path)

            self._debug('Creating installer...')
            self._runner.run(command, cwd=temp_dir)

            out_file = os.path.join(temp_dir, '{}.exe'.format(installer_name))

            self._debug('Copying {} to target path...'.format(out_file))
            shutil.copyfile(out_file, destination)
            self._debug('Finished copying installer to target path: {}'.format(
                destination))

            self._debug('Packaged successfully.', package=destination)

            return destination

        finally:
            utils.rmf(temp_dir)