summaryrefslogtreecommitdiff
path: root/floss/build/build-in-podman.py
blob: ba4591482ebfba0a7e819578aa5aaf804ae1cc9a (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
#!/usr/bin/env python3

import argparse
import os
import subprocess
import sys
import time

SRC_MOUNT = "/root/src"
STAGING_MOUNT = "/root/.floss"


class FlossPodmanRunner:
    """Runs Floss build inside podman container."""

    # Commands to run for build
    BUILD_COMMANDS = [
        # First run bootstrap to get latest code + create symlinks
        [f'{SRC_MOUNT}/build.py', '--run-bootstrap'],

        # Clean up any previous artifacts inside the volume
        [f'{SRC_MOUNT}/build.py', '--target', 'clean'],

        # Run normal code builder
        [f'{SRC_MOUNT}/build.py', '--target', 'all'],

        # Run tests
        [f'{SRC_MOUNT}/build.py', '--target', 'test'],
    ]

    def __init__(self, workdir, rootdir, image_tag, volume_name, container_name, staging_dir):
        """ Constructor.

        Args:
            workdir: Current working directory (should be the script path).
            rootdir: Root directory for Bluetooth.
            image_tag: Tag for podman image used for building.
            volume_name: Volume name used for storing artifacts.
            container_name: Name for running container instance.
            staging_dir: Directory to mount for artifacts instead of using volume.
        """
        self.workdir = workdir
        self.rootdir = rootdir
        self.image_tag = image_tag
        self.env = os.environ.copy()

        # Name of running container
        self.container_name = container_name

        # Name of volume to write output.
        self.volume_name = volume_name
        # Staging dir where we send output instead of the volume.
        self.staging_dir = staging_dir

    def run_command(self, target, args, cwd=None, env=None, ignore_rc=False):
        """ Run command and stream the output.
        """
        # Set some defaults
        if not cwd:
            cwd = self.workdir
        if not env:
            env = self.env

        rc = 0
        process = subprocess.Popen(args, cwd=cwd, env=env, stdout=subprocess.PIPE)
        while True:
            line = process.stdout.readline()
            print(line.decode('utf-8'), end="")
            if not line:
                rc = process.poll()
                if rc is not None:
                    break

                time.sleep(0.1)

        if rc != 0 and not ignore_rc:
            raise Exception("{} failed. Return code is {}".format(target, rc))

    def _create_volume_if_needed(self):
        # Check if the volume exists. Otherwise create it.
        try:
            subprocess.check_output(['podman', 'volume', 'inspect', self.volume_name])
        except:
            self.run_command('podman volume create', ['podman', 'volume', 'create', self.volume_name])

    def start_container(self):
        """Starts the podman container with correct mounts."""
        # Stop any previously started container.
        self.stop_container(ignore_error=True)

        # Create volume and create mount string
        if self.staging_dir:
            mount_output_volume = 'type=bind,src={},dst={}'.format(self.staging_dir, STAGING_MOUNT)
        else:
            # If not using staging dir, use the volume instead
            self._create_volume_if_needed()
            mount_output_volume = 'type=volume,src={},dst={}'.format(self.volume_name, STAGING_MOUNT)

        # Mount the source directory
        mount_src_dir = 'type=bind,src={},dst={}'.format(self.rootdir, SRC_MOUNT)

        # Run the podman image. It will run `tail` indefinitely so the container
        # doesn't close and we can run `podman exec` on it.
        self.run_command('podman run', [
            'podman', 'run', '--name', self.container_name, '--mount', mount_output_volume, '--mount', mount_src_dir,
            '-d', self.image_tag, 'tail', '-f', '/dev/null'
        ])

    def stop_container(self, ignore_error=False):
        """Stops the podman container for build."""
        self.run_command('podman stop', ['podman', 'stop', '-t', '1', self.container_name], ignore_rc=ignore_error)
        self.run_command('podman rm', ['podman', 'rm', self.container_name], ignore_rc=ignore_error)

    def do_build(self):
        """Runs the basic build commands."""
        # Start container before building
        self.start_container()

        try:
            # Run all commands
            for i, cmd in enumerate(self.BUILD_COMMANDS):
                self.run_command('podman exec #{}'.format(i), ['podman', 'exec', '-it', self.container_name] + cmd)
        finally:
            # Always stop container before exiting
            self.stop_container()

    def print_do_build(self):
        """Prints the commands for building."""
        podman_exec = ['podman', 'exec', '-it', self.container_name]
        print('Normally, build would run the following commands: \n')
        for cmd in self.BUILD_COMMANDS:
            print(' '.join(podman_exec + cmd))

    def check_podman_runnable(self):
        try:
            subprocess.check_output(['podman', 'ps'], stderr=subprocess.STDOUT)
        except subprocess.CalledProcessError as err:
            if 'denied' in err.output.decode('utf-8'):
                print('Run script as sudo')
            else:
                print('Unexpected error: {}'.format(err.output.decode('utf-8')))

            return False

        # No exception means podman is ok
        return True


if __name__ == "__main__":
    parser = argparse.ArgumentParser('Builder Floss inside podman image.')
    parser.add_argument(
        '--only-start',
        action='store_true',
        default=False,
        help='Only start the container. Prints the commands it would have ran.')
    parser.add_argument('--only-stop', action='store_true', default=False, help='Only stop the container and exit.')
    parser.add_argument('--image-tag', default='floss:latest', help='Podman image to use to build.')
    parser.add_argument(
        '--volume-tag',
        default='floss-out',
        help='Name of volume to use. This is where build artifacts will be stored by default.')
    parser.add_argument(
        '--staging-dir',
        default=None,
        help='Staging directory to use instead of volume. Build artifacts will be written here.')
    parser.add_argument('--container-name', default='floss-podman-runner', help='What to name the started container')
    args = parser.parse_args()

    # cwd should be set to same directory as this script (that's where Podmanfile
    # is kept).
    workdir = os.path.dirname(os.path.abspath(sys.argv[0]))
    rootdir = os.path.abspath(os.path.join(workdir, '../..'))

    # Determine staging directory absolute path
    staging = os.path.abspath(args.staging_dir) if args.staging_dir else None

    fdr = FlossPodmanRunner(workdir, rootdir, args.image_tag, args.volume_tag, args.container_name, staging)

    # Make sure podman is runnable before continuing
    if fdr.check_podman_runnable():
        # Handle some flags
        if args.only_start:
            fdr.start_container()
            fdr.print_do_build()
        elif args.only_stop:
            fdr.stop_container()
        else:
            fdr.do_build()