From 0c0b32611e749763373daf55cf7dfb3cfdaded9a Mon Sep 17 00:00:00 2001 From: "Moises Lopez - https://www.vauxoo.com/" Date: Fri, 7 Aug 2026 16:43:27 -0600 Subject: [PATCH] [IMP] travis2docker: Preinstall VS Code server and add devcontainer.json Opt-in with the DEPLOYV_VSCODE=1 environment variable so vim/terminal users do not pay the extra build time downloading VS Code artifacts: - The Dockerfile pre-installs the VS Code server pinned to the commit of the local 'code' binary (falling back to the latest stable) and the extensions of templates/.vscode/extensions.json at build time, so attaching VS Code to the container does not install anything live - Generate a .devcontainer.json next to the Dockerfile pointing to the image built by 10-build.sh so VS Code offers 'Reopen in Container' Based on the idea of Vauxoo/travis2docker#169 by @antonag32 Related to Vauxoo/travis2docker#158 --- README.rst | 22 +++++++++ src/travis2docker/cli.py | 5 ++ .../templates/Dockerfile_deployv | 18 ++++++++ src/travis2docker/templates/devcontainer.json | 14 ++++++ src/travis2docker/travis2docker.py | 34 ++++++++++++++ tests/test_travis2docker.py | 46 +++++++++++++++++++ 6 files changed, 139 insertions(+) create mode 100644 src/travis2docker/templates/devcontainer.json diff --git a/README.rst b/README.rst index db44839..01c4a1a 100644 --- a/README.rst +++ b/README.rst @@ -119,6 +119,28 @@ Example enabling more than one:: travisfile2dockerfile --build-env-args VIM_INSTALL ZSH_INSTALL \ git@github.com:Vauxoo/forecast.git 8.0 +VS Code support (``DEPLOYV_VSCODE``) +==================================== + +Opt-in with the environment variable ``DEPLOYV_VSCODE=1``:: + + DEPLOYV_VSCODE=1 travisfile2dockerfile git@github.com:Vauxoo/forecast.git 8.0 + +It is disabled by default so vim/terminal users do not pay the extra build +time. When enabled: + +- The Dockerfile pre-installs the VS Code server and the extensions listed in + ``templates/.vscode/extensions.json`` at **build** time, so attaching VS Code + to the container does not download anything live. The server version is + pinned to the commit of your local ``code`` binary when available (run + ``travisfile2dockerfile`` again after upgrading VS Code to re-pin it); + otherwise the latest stable server is used. If the pinned server does not + match your client, VS Code just falls back to downloading its own version; + the pre-installed extensions are version-independent and are reused anyway. +- A ``.devcontainer.json`` is generated next to the Dockerfile pointing to the + image built by ``10-build.sh``, so opening that folder in VS Code offers + "Reopen in Container" automatically. + codebase-memory-deployv ======================= diff --git a/src/travis2docker/cli.py b/src/travis2docker/cli.py index f077b9f..112d443 100644 --- a/src/travis2docker/cli.py +++ b/src/travis2docker/cli.py @@ -245,6 +245,10 @@ def main(return_result=False): os_kwargs.update({"remotes": remotes, "git_base": git_base}) if docker_user: os_kwargs.update({"user": docker_user}) + # Opt-in with DEPLOYV_VSCODE=1: pre-install the VS Code server and extensions in the + # image and generate a .devcontainer.json. Disabled by default so e.g. vim users + # do not pay the extra build time downloading VS Code stuff they will never use + vscode = os.environ.get("DEPLOYV_VSCODE", "").strip().lower() not in ("", "0", "false", "no") t2d = Travis2Docker( work_path=pathlib.Path(root_path) / "script" / GitRun.url2dirname(git_repo) / revision, image=default_docker_image, @@ -252,6 +256,7 @@ def main(return_result=False): copy_paths=[(pathlib.Path("~/.ssh").expanduser(), "$HOME/.ssh")] + rcfiles, build_env_args=build_env_args, build_extra_steps=args.build_extra_steps, + vscode=vscode, ) t2d.build_extra_params = { "extra_params": build_extra_args, diff --git a/src/travis2docker/templates/Dockerfile_deployv b/src/travis2docker/templates/Dockerfile_deployv index 42f12e8..259e4fe 100644 --- a/src/travis2docker/templates/Dockerfile_deployv +++ b/src/travis2docker/templates/Dockerfile_deployv @@ -41,6 +41,24 @@ RUN . /home/odoo/build.sh && \ [ -e /entrypoint ] && mv /entrypoint /deployv_entrypoint ; \ mkdir -p /run/sshd +{% if vscode %} +# DEPLOYV_VSCODE: pre-install the VS Code server and extensions at build time +# so attaching VS Code to the container does not download anything live +RUN arch=$(uname -m) && case "$arch" in x86_64) arch=x64 ;; aarch64|arm64) arch=arm64 ;; esac && \ + tmp_dir=$(mktemp -d) && \ + curl -fsSL "https://update.code.visualstudio.com/{{ vscode_version }}/server-linux-$arch/stable" \ + | tar -xz -C "$tmp_dir" --strip-components=1 && \ + commit=$(sed -n 's/.*"commit": *"\([^"]*\)".*/\1/p' "$tmp_dir/product.json") && \ + mkdir -p /home/odoo/.vscode-server/bin && \ + mv "$tmp_dir" "/home/odoo/.vscode-server/bin/$commit" && \ + "/home/odoo/.vscode-server/bin/$commit/bin/code-server" \ + --extensions-dir /home/odoo/.vscode-server/extensions \ + --user-data-dir /home/odoo/.vscode-server/data \ + {%- for extension in vscode_extensions %} + --install-extension {{ extension }} \ + {%- endfor %} + && chown -R {{ user }}:{{ user }} /home/odoo/.vscode-server +{% endif %} {% for step in build_extra_steps %} RUN {{ step }} {% endfor %} diff --git a/src/travis2docker/templates/devcontainer.json b/src/travis2docker/templates/devcontainer.json new file mode 100644 index 0000000..8d29b4a --- /dev/null +++ b/src/travis2docker/templates/devcontainer.json @@ -0,0 +1,14 @@ +{ + "name": "{{ name }}", + "image": "{{ image }}", + "remoteUser": "{{ user }}", + "workspaceFolder": "/home/odoo/instance", + "customizations": { + "vscode": { + "extensions": {{ vscode_extensions | tojson }}, + "settings": { + "python.analysis.extraPaths": ["/home/odoo/instance/odoo"] + } + } + } +} diff --git a/src/travis2docker/travis2docker.py b/src/travis2docker/travis2docker.py index 14b20f8..8864537 100644 --- a/src/travis2docker/travis2docker.py +++ b/src/travis2docker/travis2docker.py @@ -1,8 +1,10 @@ # pylint: disable=useless-object-inheritance,consider-using-with,print-used +import json import pathlib import re import shutil import stat +import subprocess from tempfile import gettempdir import jinja2 @@ -39,6 +41,20 @@ def run_template(self): def chmod_execution(file_path): file_path.chmod(file_path.stat().st_mode | stat.S_IEXEC) + @staticmethod + def get_vscode_version(): + """Return "commit:SHA" of the local VS Code client to pre-install the matching + server into the image, or "latest" if there is no "code" binary available""" + code_bin = shutil.which("code") + if code_bin: + try: + lines = subprocess.check_output([code_bin, "--version"]).decode("UTF-8").splitlines() + if len(lines) >= 2 and lines[1].strip(): + return "commit:%s" % lines[1].strip() + except (subprocess.CalledProcessError, OSError): + pass + return "latest" + def __init__( self, image=None, @@ -49,8 +65,10 @@ def __init__( copy_paths=None, build_env_args=None, build_extra_steps=None, + vscode=False, ): self.curr_work_path = None + self.vscode = vscode self.build_extra_params = {} self.run_extra_params = {} self.build_env_args = build_env_args @@ -73,6 +91,10 @@ def __init__( copy_paths.append([templates_dir / ".vscode", "/home/odoo/.vscode"]) copy_paths.append([templates_dir / ".coveragerc", "/home/odoo/.coveragerc"]) os_kwargs.setdefault("user", "odoo") + if self.vscode: + extensions_data = json.loads((templates_dir / ".vscode" / "extensions.json").read_text()) + os_kwargs.setdefault("vscode_extensions", extensions_data["recommendations"]) + os_kwargs.setdefault("vscode_version", self.get_vscode_version()) if dockerfile is None: dockerfile = "Dockerfile" if templates_path is None: @@ -113,16 +135,28 @@ def compute_dockerfile(self): "image": self.image, "build_env_args": self.build_env_args, "build_extra_steps": self.build_extra_steps, + "vscode": self.vscode, } kwargs.update(self.os_kwargs) with curr_dockerfile.open("w") as f_dockerfile: dockerfile_content = self.dockerfile_template.render(kwargs).strip("\n ") f_dockerfile.write(dockerfile_content) + if self.vscode: + self.compute_devcontainer() self.compute_build_scripts() work_paths = [str(self.curr_work_path)] self.curr_work_path = None return work_paths + def compute_devcontainer(self): + """Generate a .devcontainer.json so VS Code offers reopening the generated + image as a Development Container""" + name = self.os_kwargs.get("repo_project") or self.variables_sh_data.get("main_app", "odoo") + devcontainer_content = self.jinja_env.get_template("devcontainer.json").render( + image=self.new_image, name=name, **self.os_kwargs + ) + (self.curr_work_path / ".devcontainer.json").write_text(devcontainer_content) + def copy_path(self, path): """:param paths list: List of paths to copy""" src = pathlib.Path(path).expanduser() diff --git a/tests/test_travis2docker.py b/tests/test_travis2docker.py index 630dfd8..f5b5b45 100644 --- a/tests/test_travis2docker.py +++ b/tests/test_travis2docker.py @@ -1,6 +1,7 @@ # No logger planned to use here # pylint: disable=print-used,consider-using-with +import json import os import pathlib import subprocess @@ -103,6 +104,51 @@ def test_main_deployv(tmp_path, monkeypatch): check_dockerfile_lint(scripts) +def test_main_deployv_vscode(tmp_path, monkeypatch): + monkeypatch.chdir(tmp_path) + monkeypatch.setenv("DEPLOYV_VSCODE", "1") + repo = create_repo(tmp_path, {"variables.sh": VARIABLES_SH}) + sys.argv = [ + "travis2docker", + repo, + "main", + "--root-path", + str(tmp_path / "t2d"), + ] + scripts = cli_main(return_result=True) + assert len(scripts) == 1, "Scripts returned should be 1" + work_path = pathlib.Path(scripts[0]) + dkr_content = (work_path / "Dockerfile").read_text() + assert "update.code.visualstudio.com" in dkr_content + assert "/home/odoo/.vscode-server" in dkr_content + assert "--install-extension ms-python.python" in dkr_content + devcontainer = json.loads((work_path / ".devcontainer.json").read_text()) + assert devcontainer["name"] == "myproject" + assert devcontainer["remoteUser"] == "odoo" + assert devcontainer["workspaceFolder"] == "/home/odoo/instance" + assert devcontainer["image"].endswith(":main") + assert "ms-python.python" in devcontainer["customizations"]["vscode"]["extensions"] + check_dockerfile_lint(scripts) + + +def test_main_deployv_without_vscode(tmp_path, monkeypatch): + monkeypatch.chdir(tmp_path) + monkeypatch.delenv("DEPLOYV_VSCODE", raising=False) + repo = create_repo(tmp_path, {"variables.sh": VARIABLES_SH}) + sys.argv = [ + "travis2docker", + repo, + "main", + "--root-path", + str(tmp_path / "t2d"), + ] + scripts = cli_main(return_result=True) + work_path = pathlib.Path(scripts[0]) + dkr_content = (work_path / "Dockerfile").read_text() + assert ".vscode-server" not in dkr_content + assert not (work_path / ".devcontainer.json").exists() + + def test_main_docker_image_parameter(tmp_path, monkeypatch): monkeypatch.chdir(tmp_path) repo = create_repo(tmp_path, {"variables.sh": VARIABLES_SH})