diff --git a/README.rst b/README.rst index 8e87e40..4eefc67 100644 --- a/README.rst +++ b/README.rst @@ -122,6 +122,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 08e73da..c19ea9d 100644 --- a/src/travis2docker/cli.py +++ b/src/travis2docker/cli.py @@ -284,6 +284,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, @@ -291,6 +295,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 e2a4781..4c5f3dc 100644 --- a/src/travis2docker/travis2docker.py +++ b/src/travis2docker/travis2docker.py @@ -1,9 +1,11 @@ # pylint: disable=useless-object-inheritance,consider-using-with +import json import logging import pathlib import re import shutil import stat +import subprocess from tempfile import gettempdir import jinja2 @@ -42,6 +44,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, @@ -52,8 +68,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 @@ -76,6 +94,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: @@ -116,16 +138,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 c0e4412..f91c3fb 100644 --- a/tests/test_travis2docker.py +++ b/tests/test_travis2docker.py @@ -1,5 +1,6 @@ # pylint: disable=consider-using-with +import json import os import pathlib import subprocess @@ -104,6 +105,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})