150 Add support for repos that use submodules.

Allow though submodules are a contentious topic, this adds a submodule_aware_builder

This can be used with the following config in tito.props:

```
[buildconfig]
builder = SubmoduleAwareBuilder
```
This commit is contained in:
Allen Reese 2021-05-05 10:47:38 -07:00 committed by Jakub Kadlčík
parent 88699fdf0b
commit f4251b30cd

View file

@ -0,0 +1,210 @@
# Copyright (c) 2008-2011 Red Hat, Inc.
#
# This software is licensed to you under the GNU General Public License,
# version 2 (GPLv2). There is NO WARRANTY for this software, express or
# implied, including the implied warranties of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. You should have received a copy of GPLv2
# along with this software; if not, see
# http://www.gnu.org/licenses/old-licenses/gpl-2.0.txt.
#
# Red Hat trademarks are not licensed under GPLv2. No permission is
# granted to use or replicate Red Hat trademarks that are incorporated
# in this software or its documentation.
"""
Tito builders for a variety of common methods of building sources, srpms,
and rpms.
"""
import os
from tito.builder import Builder
from tito.common import debug, run_command, create_tgz, find_spec_like_file, get_commit_timestamp
from tito.tar import TarFixer
class PushDir(object):
"""
Helper class to change directories and restore the same way pushd/popd work in the shell.
Usage:
# cwd = 'current'
with PushDir("into"):
# cwd = 'current/into'
do_command # cwd is into.
# cwd = 'current'
"""
def __init__(self, subdir):
self.old_wd = os.getcwd()
self.new_wd = None
if subdir:
self.new_wd = os.path.join(self.old_wd, subdir)
def __enter__(self):
if self.new_wd:
self.chdir(self.old_wd, self.new_wd)
def __exit__(self, type, value, traceback):
if self.new_wd:
self.chdir(self.new_wd, self.old_wd)
def chdir(self, old_dir, new_dir):
if new_dir:
debug('Changing directory from %s to %s' % (old_dir, new_dir))
os.chdir(new_dir)
class SubmoduleAwareBuilder(Builder):
"""
Submodule aware builder class.
This builder is aware of submodules and will package the contents of the submodules as well.
It expectes `git submodule--helper list` to exist and work.
"""
REQUIRED_ARGS = []
# TODO: drop version
def __init__(self, name=None, tag=None, build_dir=None,
config=None, user_config=None,
args=None, **kwargs):
# call the parent super class and let it configure everything
Builder.__init__(self, name=name, build_dir=build_dir, config=config,
user_config=user_config, args=args, **kwargs)
"""
name - Package name that is being built.
version - Version and release being built.
tag - The git tag being built.
build_dir - Temporary build directory where we can safely work.
config - Merged configuration. (global plus package specific)
user_config - User configuration from ~/.titorc.
args - Optional arguments specific to each builder. Can be passed
in explicitly by user on the CLI, or via a release target config
entry. Only for things which vary on invocations of the builder,
avoid using these if possible. *Given in the format of a dictionary
of lists.*
"""
def _setup_sources(self):
"""
Create a copy of the git source for the project at the point in time
our build tag was created.
Created in the temporary rpmbuild SOURCES directory.
*This is the only thing that SubmoduleAwareBuilder changes*.
*It needs to call a different create_tgz
"""
self._create_build_dirs()
debug("Creating %s from git tag: %s..." % (self.tgz_filename,
self.git_commit_id))
# call our create_tgz, instead of the global one.
self.create_tgz(self.git_root, self.tgz_dir, self.git_commit_id,
self.relative_project_dir,
os.path.join(self.rpmbuild_sourcedir, self.tgz_filename))
# Extract the source so we can get at the spec file, etc.
debug("Copying git source to: %s" % self.rpmbuild_gitcopy)
run_command("cd %s/ && tar xzf %s" % (self.rpmbuild_sourcedir,
self.tgz_filename))
debug("Show contents of the directory structure we just extracted:\n%s"
% os.listdir(self.rpmbuild_gitcopy))
# NOTE: The spec file we actually use is the one exported by git
# archive into the temp build directory. This is done so we can
# modify the version/release on the fly when building test rpms
# that use a git SHA1 for their version.
self.spec_file_name = os.path.basename(find_spec_like_file(self.rpmbuild_gitcopy))
self.spec_file = os.path.join(
self.rpmbuild_gitcopy, self.spec_file_name)
def run_git_archive(self, relative_git_dir, prefix, commit, dest_tar, subdir):
# command to generate a git-archive
git_archive_cmd = 'git archive --format=tar --prefix=%s/ %s:%s --output=%s' % (
prefix, commit, relative_git_dir, dest_tar)
with PushDir(subdir) as p:
run_command(git_archive_cmd)
# Run git-archive separately if --debug was specified.
# This allows us to detect failure early.
# On git < 1.7.4-rc0, `git archive ... commit:./` fails!
debug('git-archive fails if relative dir is not in git tree',
'%s > /dev/null' % git_archive_cmd)
def create_tgz(self, git_root, prefix, commit, relative_dir,
dest_tgz):
"""
Create a .tar.gz from a projects source in git.
And include submodules
"""
git_root_abspath = os.path.abspath(git_root)
gitmodules_path = os.path.join(git_root_abspath, '.gitmodules')
# if .gitmodules does not exist, just call the existing create_tgz function
# as there is nothing to see here.
if not os.path.exists(gitmodules_path):
return create_tgz(git_root, prefix, commit, relative_dir, dest_tgz)
os.chdir(git_root_abspath)
timestamp = get_commit_timestamp(commit)
# Accommodate standalone projects with specfile in root of git repo:
relative_git_dir = "%s" % relative_dir
if relative_git_dir in ['/', './']:
relative_git_dir = ""
basename = os.path.splitext(dest_tgz)[0]
initial_tar = "%s.initial" % basename
# We need to tar up the following:
# 1. the current repo
self.run_git_archive(relative_git_dir, prefix, commit, initial_tar, None)
# 2. all of the submodules
# then combine those into a single archive.
submodules_cmd = 'git submodule--helper list'
submodules_output = run_command(submodules_cmd)
# split submodules output on newline
# then on tab, and the directory is the last entry
submodules_list = [line.split('\t')[-1] for line in submodules_output.split('\n')]
submodule_tar_files = [initial_tar]
# We ignore the hash in the sub modules list as we'll have to get the correct one
# from the commit id in commit
for submodule in submodules_list:
# to find the submodule shars:
# git rev-parse <commit>:./<submodule>
rev_parse_cmd = 'git rev-parse %s:./%s' % (commit, submodule)
submodule_commit = run_command(rev_parse_cmd)
submodule_tar_file = '%s.%s' % (initial_tar, submodule)
# prefix should be <prefix>/<submodule>
submodule_prefix = '%s/%s' % (prefix, submodule)
self.run_git_archive(relative_git_dir, submodule_prefix, submodule_commit,
submodule_tar_file, submodule)
submodule_tar_files.append(submodule_tar_file)
# we need to append all of the submodule tar files onto the initial
tarfiles = ' '.join(submodule_tar_files)
run_command("tar -Af %s" % tarfiles)
fixed_tar = "%s.tar" % basename
fixed_tar_fh = open(fixed_tar, 'wb')
try:
tarfixer = TarFixer(open(initial_tar, 'rb'), fixed_tar_fh, timestamp, commit)
tarfixer.fix()
finally:
fixed_tar_fh.close()
# It's a pity we can't use Python's gzip, but it doesn't offer an equivalent of -n
return run_command("gzip -n -c < %s > %s" % (fixed_tar, dest_tgz))