Browse Source

support --build-arg for build command

Signed-off-by: Dat Tran <[email protected]>
Dat Tran 9 years ago
parent
commit
449dcc9d7b

+ 14 - 6
compose/cli/main.py

@@ -209,18 +209,26 @@ class TopLevelCommand(object):
         e.g. `composetest_db`. If you change a service's `Dockerfile` or the
         contents of its build directory, you can run `docker-compose build` to rebuild it.
 
-        Usage: build [options] [SERVICE...]
+        Usage: build [options] [--build-arg key=val...] [SERVICE...]
 
         Options:
-            --force-rm  Always remove intermediate containers.
-            --no-cache  Do not use cache when building the image.
-            --pull      Always attempt to pull a newer version of the image.
+            --force-rm              Always remove intermediate containers.
+            --no-cache              Do not use cache when building the image.
+            --pull                  Always attempt to pull a newer version of the image.
+            --build-arg key=val     Set build-time variables for one service.
         """
+        service_names = options['SERVICE']
+        build_args = options.get('--build-arg', None)
+
+        if not service_names and build_args:
+            raise UserError("Need service name for --build-arg option")
+
         self.project.build(
-            service_names=options['SERVICE'],
+            service_names=service_names,
             no_cache=bool(options.get('--no-cache', False)),
             pull=bool(options.get('--pull', False)),
-            force_rm=bool(options.get('--force-rm', False)))
+            force_rm=bool(options.get('--force-rm', False)),
+            build_args=build_args)
 
     def bundle(self, config_options, options):
         """

+ 1 - 0
compose/config/__init__.py

@@ -7,5 +7,6 @@ from .config import ConfigurationError
 from .config import DOCKER_CONFIG_KEYS
 from .config import find
 from .config import load
+from .config import merge_build_args
 from .config import merge_environment
 from .config import parse_environment

+ 6 - 0
compose/config/config.py

@@ -602,6 +602,12 @@ def resolve_environment(service_dict, environment=None):
     return dict(resolve_env_var(k, v, environment) for k, v in six.iteritems(env))
 
 
+def merge_build_args(base, override, environment):
+    override_args = parse_build_arguments(override)
+    override_dict = dict(resolve_env_var(k, v, environment) for k, v in six.iteritems(override_args))
+    base.update(override_dict)
+
+
 def resolve_build_args(build, environment):
     args = parse_build_arguments(build.get('args'))
     return dict(resolve_env_var(k, v, environment) for k, v in six.iteritems(args))

+ 2 - 2
compose/project.py

@@ -307,10 +307,10 @@ class Project(object):
             'Restarting')
         return containers
 
-    def build(self, service_names=None, no_cache=False, pull=False, force_rm=False):
+    def build(self, service_names=None, no_cache=False, pull=False, force_rm=False, build_args=None):
         for service in self.get_services(service_names):
             if service.can_be_built():
-                service.build(no_cache, pull, force_rm)
+                service.build(no_cache, pull, force_rm, build_args)
             else:
                 log.info('%s uses an image, skipping' % service.name)
 

+ 9 - 3
compose/service.py

@@ -21,6 +21,7 @@ from . import __version__
 from . import const
 from . import progress_stream
 from .config import DOCKER_CONFIG_KEYS
+from .config import merge_build_args
 from .config import merge_environment
 from .config.types import ServicePort
 from .config.types import VolumeSpec
@@ -803,13 +804,18 @@ class Service(object):
 
         return [build_spec(secret) for secret in self.secrets]
 
-    def build(self, no_cache=False, pull=False, force_rm=False):
+    def build(self, no_cache=False, pull=False, force_rm=False, build_args=None):
         log.info('Building %s' % self.name)
 
         build_opts = self.options.get('build', {})
-        path = build_opts.get('context')
+
+        self_args_opts = build_opts.get('args', None)
+        if self_args_opts and build_args:
+            merge_build_args(self_args_opts, build_args, self.options.get('environment'))
+
         # python2 os.stat() doesn't support unicode on some UNIX, so we
         # encode it to a bytestring to be safe
+        path = build_opts.get('context')
         if not six.PY3 and not IS_WINDOWS_PLATFORM:
             path = path.encode('utf8')
 
@@ -822,8 +828,8 @@ class Service(object):
             pull=pull,
             nocache=no_cache,
             dockerfile=build_opts.get('dockerfile', None),
-            buildargs=build_opts.get('args', None),
             cache_from=build_opts.get('cache_from', None),
+            buildargs=self_args_opts
         )
 
         try:

+ 19 - 0
tests/unit/config/config_test.py

@@ -15,6 +15,7 @@ import yaml
 from ...helpers import build_config_details
 from compose.config import config
 from compose.config import types
+from compose.config.config import merge_build_args
 from compose.config.config import resolve_build_args
 from compose.config.config import resolve_environment
 from compose.config.config import V1
@@ -2881,6 +2882,24 @@ class EnvTest(unittest.TestCase):
             {'arg1': 'value1', 'empty_arg': '', 'env_arg': 'value2', 'no_env': None},
         )
 
+    @mock.patch.dict(os.environ)
+    def test_merge_build_args(self):
+        os.environ['env_arg'] = 'value2'
+
+        base = {
+            'arg1': 'arg1_value',
+            'arg2': 'arg2_value'
+        }
+        override = {
+            'arg1': 'arg1_new_value',
+            'arg2': 'arg2_value'
+        }
+        self.assertEqual(base['arg1'], 'arg1_value')
+
+        merge_build_args(base, override, os.environ)
+
+        self.assertEqual(base['arg1'], 'arg1_new_value')
+
     @pytest.mark.xfail(IS_WINDOWS_PLATFORM, reason='paths use slash')
     @mock.patch.dict(os.environ)
     def test_resolve_path(self):

+ 19 - 0
tests/unit/service_test.py

@@ -513,6 +513,25 @@ class ServiceTest(unittest.TestCase):
         self.assertEqual(self.mock_client.build.call_count, 1)
         self.assertFalse(self.mock_client.build.call_args[1]['pull'])
 
+    def test_build_with_override_build_args(self):
+        self.mock_client.build.return_value = [
+            b'{"stream": "Successfully built 12345"}',
+        ]
+
+        build_args = [
+            'arg1=arg1_new_value',
+            'arg2=arg2_value'
+        ]
+        service = Service('foo', client=self.mock_client,
+                          build={'context': '.', 'args': {'arg1': 'arg1', 'arg2': 'arg2'}})
+        service.build(build_args=build_args)
+
+        called_build_args = self.mock_client.build.call_args[1]['buildargs']
+
+        for arg in called_build_args:
+            if "arg1=" in arg:
+                self.assertEquals(arg, 'arg1=arg1_new_value')
+
     def test_config_dict(self):
         self.mock_client.inspect_image.return_value = {'Id': 'abcd'}
         service = Service(