vscode-offline 0.1.2__py3-none-any.whl → 0.1.4__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
vscode_offline/app.py CHANGED
@@ -6,6 +6,7 @@ from argparse import ArgumentParser, Namespace
6
6
  from pathlib import Path
7
7
 
8
8
  from vscode_offline.download import (
9
+ download_vscode_client,
9
10
  download_vscode_extensions,
10
11
  download_vscode_server,
11
12
  )
@@ -19,7 +20,7 @@ from vscode_offline.utils import (
19
20
  get_host_platform,
20
21
  get_vscode_cli_bin,
21
22
  get_vscode_commit_from_code_version,
22
- get_vscode_commit_from_installer,
23
+ get_vscode_commit_from_server_installer,
23
24
  get_vscode_extensions_config,
24
25
  get_vscode_server_home,
25
26
  )
@@ -36,7 +37,7 @@ def cmd_download_server(args: Namespace) -> None:
36
37
 
37
38
  download_vscode_server(
38
39
  args.commit,
39
- output=args.installer / f"cli-{args.commit}",
40
+ output=args.installer / f"server-{args.commit}",
40
41
  target_platform=args.target_platform,
41
42
  )
42
43
  extensions_config = Path(args.extensions_config).expanduser()
@@ -49,7 +50,7 @@ def cmd_install_server(args: Namespace) -> None:
49
50
  host_platform = get_host_platform()
50
51
  if args.commit is None:
51
52
  try:
52
- args.commit = get_vscode_commit_from_installer(
53
+ args.commit = get_vscode_commit_from_server_installer(
53
54
  args.installer, host_platform
54
55
  )
55
56
  except Exception as e:
@@ -59,7 +60,7 @@ def cmd_install_server(args: Namespace) -> None:
59
60
 
60
61
  install_vscode_server(
61
62
  args.commit,
62
- cli_installer=args.installer / f"cli-{args.commit}",
63
+ server_installer=args.installer / f"server-{args.commit}",
63
64
  vscode_cli_bin=get_vscode_cli_bin(args.commit),
64
65
  platform=host_platform,
65
66
  )
@@ -88,6 +89,26 @@ def cmd_install_extensions(args: Namespace) -> None:
88
89
  )
89
90
 
90
91
 
92
+ def cmd_download_client(args: Namespace) -> None:
93
+ if args.commit is None:
94
+ args.commit = get_vscode_commit_from_code_version()
95
+ if args.commit is None:
96
+ logger.info(
97
+ "Cannot determine commit from `code --version`, please specify --commit manually."
98
+ )
99
+ raise ValueError("Please specify --commit when installing.")
100
+
101
+ download_vscode_client(
102
+ args.commit,
103
+ output=args.installer / f"client-{args.commit}",
104
+ target_platform=args.target_platform,
105
+ )
106
+ extensions_config = Path(args.extensions_config).expanduser()
107
+ download_vscode_extensions(
108
+ extensions_config, args.target_platform, args.installer / "extensions"
109
+ )
110
+
111
+
91
112
  def make_argparser() -> ArgumentParser:
92
113
  parent_parser = ArgumentParser(add_help=False)
93
114
 
@@ -169,6 +190,30 @@ def make_argparser() -> ArgumentParser:
169
190
  help="Path to the `code` binary.",
170
191
  )
171
192
 
193
+ download_client_parser = subparsers.add_parser(
194
+ "download-client",
195
+ help="Download VS Code and extensions",
196
+ parents=[parent_parser],
197
+ )
198
+ download_client_parser.set_defaults(func=cmd_download_client)
199
+ download_client_parser.add_argument(
200
+ "--commit",
201
+ type=str,
202
+ help="The commit hash of the VS Code to download, must match the version of the VSCode client.",
203
+ )
204
+ download_client_parser.add_argument(
205
+ "--target-platform",
206
+ type=str,
207
+ required=True,
208
+ help="The target platform of the VS Code to download.",
209
+ )
210
+ download_client_parser.add_argument(
211
+ "--extensions-config",
212
+ type=Path,
213
+ default=get_vscode_extensions_config(),
214
+ help="Path to the extensions configuration file. Will search for extensions to download.",
215
+ )
216
+
172
217
  return parser
173
218
 
174
219
 
@@ -3,14 +3,20 @@ from __future__ import annotations
3
3
  import json
4
4
  import os
5
5
  from gzip import GzipFile
6
+ from io import DEFAULT_BUFFER_SIZE
7
+ from pathlib import Path
6
8
  from urllib.error import HTTPError
7
9
  from urllib.request import urlopen
8
10
 
9
11
  from vscode_offline.loggers import logger
10
- from vscode_offline.utils import get_cli_os_arch
12
+ from vscode_offline.utils import get_cli_platform, get_filename_from_headers
11
13
 
12
14
 
13
- def _download_file(url: str, filename: str) -> None:
15
+ def _download_file(
16
+ url: str,
17
+ directory: str | os.PathLike[str],
18
+ filename: str | None = None,
19
+ ) -> os.PathLike[str]:
14
20
  with urlopen(url) as resp:
15
21
  content_encoding = resp.headers.get("Content-Encoding")
16
22
  if content_encoding in {"gzip", "deflate"}:
@@ -21,39 +27,56 @@ def _download_file(url: str, filename: str) -> None:
21
27
  else:
22
28
  raise ValueError(f"Unsupported Content-Encoding: {content_encoding}")
23
29
 
24
- with reader, open(filename, "wb") as fp:
30
+ if filename:
31
+ file_path = Path(directory).joinpath(filename)
32
+ else:
33
+ filename = get_filename_from_headers(resp.headers)
34
+ if not filename:
35
+ raise ValueError(
36
+ "Cannot get filename from HTTP headers, please specify argument `filename`."
37
+ )
38
+ logger.info(f"Get filename `{filename}` from HTTP headers.")
39
+ file_path = Path(directory).joinpath(filename)
40
+ if file_path.exists():
41
+ logger.info(f"File {file_path} already exists, skipping download.")
42
+ return file_path
43
+
44
+ tmp_file_path = Path(directory).joinpath(f"{filename}.tmp")
45
+ with reader, tmp_file_path.open("wb") as fp:
25
46
  while True:
26
- chunk = reader.read(1024)
47
+ chunk = reader.read(DEFAULT_BUFFER_SIZE)
27
48
  if not chunk:
28
49
  break
29
50
  fp.write(chunk)
30
51
 
52
+ if os.path.exists(file_path):
53
+ os.remove(file_path)
54
+ os.rename(tmp_file_path, file_path)
55
+
56
+ logger.info(f"Saved to {file_path} .")
57
+ return file_path
58
+
31
59
 
32
60
  def download_file(
33
61
  url: str,
34
- filename: str,
62
+ directory: str | os.PathLike[str],
63
+ filename: str | None = None,
35
64
  ) -> None:
36
- if os.path.exists(filename):
37
- logger.info(f"File {filename} already exists, skipping download.")
38
- return
39
-
40
- logger.info(f"Downloading {url}")
41
- tmp_filename = f"{filename}.tmp"
65
+ if filename:
66
+ file_path = Path(directory).joinpath(filename)
67
+ if file_path.exists():
68
+ logger.info(f"File {file_path} already exists, skipping download.")
69
+ return
42
70
 
71
+ logger.info(f"Downloading {url} ...")
43
72
  for i in range(3):
44
73
  try:
45
- _download_file(url, tmp_filename)
74
+ _download_file(url, directory, filename)
46
75
  break
47
76
  except Exception as e:
48
77
  if isinstance(e, HTTPError) and e.code == 404:
49
78
  raise
50
- logger.info(f"Attempt {i + 1} failed: {e}")
51
-
52
- if os.path.exists(filename):
53
- os.remove(filename)
54
- os.rename(tmp_filename, filename)
55
-
56
- logger.info(f"Saved to {filename}")
79
+ logger.info(f"Attempt {i + 1} times failed: {e}")
57
80
 
58
81
 
59
82
  def download_extension(
@@ -66,11 +89,8 @@ def download_extension(
66
89
  url = f"https://marketplace.visualstudio.com/_apis/public/gallery/publishers/{publisher}/vsextensions/{name}/{version}/vspackage"
67
90
  if platform:
68
91
  url = f"{url}?targetPlatform={platform}"
69
- filename = f"{publisher}.{name}-{version}"
70
- if platform:
71
- filename = f"{filename}@{platform}"
72
- filename = f"{filename}.vsix"
73
- download_file(url, f"{output}/{filename}")
92
+ filename = f"{publisher}.{name}-{version}{f'@{platform}' if platform else ''}.vsix"
93
+ download_file(url, output, filename)
74
94
 
75
95
 
76
96
  def download_vscode_extensions(
@@ -107,11 +127,27 @@ def download_vscode_server(
107
127
  os.makedirs(output, exist_ok=True)
108
128
  download_file(
109
129
  f"https://update.code.visualstudio.com/commit:{commit}/server-{target_platform}/stable",
110
- f"{output}/vscode-server-{target_platform}.tar.gz",
130
+ output,
131
+ f"vscode-server-{target_platform}.tar.gz",
111
132
  )
112
- target_os_arch = get_cli_os_arch(target_platform)
113
- target_os_arch_ = target_os_arch.replace("-", "_")
133
+ cli_target_platform = get_cli_platform(target_platform)
134
+ cli_target_platform_ = cli_target_platform.replace("-", "_")
135
+ download_file(
136
+ f"https://update.code.visualstudio.com/commit:{commit}/cli-{cli_target_platform}/stable",
137
+ output,
138
+ f"vscode_cli_{cli_target_platform_}_cli.tar.gz",
139
+ )
140
+
141
+
142
+ def download_vscode_client(
143
+ commit: str,
144
+ output: str,
145
+ target_platform: str,
146
+ ) -> None:
147
+ """Download VS Code for the given commit and target platform."""
148
+ os.makedirs(output, exist_ok=True)
114
149
  download_file(
115
- f"https://update.code.visualstudio.com/commit:{commit}/cli-{target_os_arch}/stable",
116
- f"{output}/vscode_cli_{target_os_arch_}_cli.tar.gz",
150
+ f"https://update.code.visualstudio.com/commit:{commit}/{target_platform}/stable",
151
+ output,
152
+ # filename is like "VSCodeSetup-x64-1.104.3.exe" for windows
117
153
  )
vscode_offline/install.py CHANGED
@@ -7,7 +7,7 @@ from pathlib import Path
7
7
  from tempfile import TemporaryDirectory
8
8
 
9
9
  from vscode_offline.loggers import logger
10
- from vscode_offline.utils import get_cli_os_arch, get_vscode_server_home
10
+ from vscode_offline.utils import get_cli_platform, get_vscode_server_home
11
11
 
12
12
  # These extensions are excluded because they are not needed in a VS Code Server.
13
13
  SERVER_EXCLUDE_EXTENSIONS = frozenset(
@@ -66,14 +66,16 @@ def install_vscode_extensions(
66
66
 
67
67
  def install_vscode_server(
68
68
  commit: str,
69
- cli_installer: str,
69
+ server_installer: str,
70
70
  vscode_cli_bin: os.PathLike[str],
71
71
  platform: str,
72
72
  ) -> None:
73
- cli_os = get_cli_os_arch(platform)
74
- cli_os_ = cli_os.replace("-", "_")
73
+ cli_platform = get_cli_platform(platform)
74
+ cli_platform_ = cli_platform.replace("-", "_")
75
75
 
76
- vscode_cli_tarball = Path(cli_installer) / f"vscode_cli_{cli_os_}_cli.tar.gz"
76
+ vscode_cli_tarball = (
77
+ Path(server_installer) / f"vscode_cli_{cli_platform_}_cli.tar.gz"
78
+ )
77
79
  with TemporaryDirectory() as tmpdir:
78
80
  subprocess.check_call(["tar", "-xzf", vscode_cli_tarball, "-C", tmpdir])
79
81
  tmpfile = Path(tmpdir) / "code"
@@ -81,9 +83,9 @@ def install_vscode_server(
81
83
  os.remove(vscode_cli_bin)
82
84
  os.makedirs(os.path.dirname(vscode_cli_bin), exist_ok=True)
83
85
  os.rename(tmpfile, vscode_cli_bin)
84
- logger.info(f"Extracted vscode_cli_{cli_os_}_cli.tar.gz to {vscode_cli_bin}")
86
+ logger.info(f"Extracted vscode_cli_{cli_platform_}_cli.tar.gz to {vscode_cli_bin}")
85
87
 
86
- vscode_server_tarball = Path(cli_installer) / f"vscode-server-{platform}.tar.gz"
88
+ vscode_server_tarball = Path(server_installer) / f"vscode-server-{platform}.tar.gz"
87
89
  vscode_server_home = get_vscode_server_home(commit)
88
90
  os.makedirs(vscode_server_home, exist_ok=True)
89
91
  subprocess.check_call(
vscode_offline/utils.py CHANGED
@@ -1,7 +1,11 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import os
4
+ import shutil
4
5
  import subprocess
6
+ import sys
7
+ from collections.abc import Mapping
8
+ from email.parser import HeaderParser
5
9
  from pathlib import Path
6
10
 
7
11
  from vscode_offline.loggers import logger
@@ -28,8 +32,12 @@ def get_vscode_extensions_config() -> os.PathLike[str]:
28
32
  return p # default to this path
29
33
 
30
34
 
31
- def get_vscode_commit_from_installer(installer: os.PathLike[str], platform: str) -> str:
32
- directories = list(Path(installer).glob(f"cli-*/vscode-server-{platform}.tar.gz"))
35
+ def get_vscode_commit_from_server_installer(
36
+ installer: os.PathLike[str], platform: str
37
+ ) -> str:
38
+ directories = list(
39
+ Path(installer).glob(f"server-*/vscode-server-{platform}.tar.gz")
40
+ )
33
41
  if len(directories) > 1:
34
42
  raise ValueError(
35
43
  f"Multiple matching installers found in {installer} for platform {platform}"
@@ -39,16 +47,26 @@ def get_vscode_commit_from_installer(installer: os.PathLike[str], platform: str)
39
47
  f"No matching installer found in {installer} for platform {platform}"
40
48
  )
41
49
 
42
- commit = directories[0].parent.name[len("cli-") :]
50
+ commit = directories[0].parent.name[len("server-") :]
43
51
  logger.info(f"Getting commit from {platform} installer: {commit}")
44
52
  return commit
45
53
 
46
54
 
47
55
  def get_vscode_commit_from_code_version() -> str | None:
48
- res = subprocess.run(["code", "--version"], stdout=subprocess.PIPE)
49
- if res.returncode != 0:
56
+ """Get the current VS Code commit hash by running `code --version`.
57
+ Returns None if `code` is not found or the output is unexpected.
58
+ """
59
+ executable = shutil.which("code")
60
+ if executable is None:
50
61
  return None
51
- lines = res.stdout.splitlines()
62
+ proc = subprocess.run(
63
+ ["code", "--version"],
64
+ executable=executable,
65
+ stdout=subprocess.PIPE,
66
+ )
67
+ if proc.returncode != 0:
68
+ return None
69
+ lines = proc.stdout.splitlines()
52
70
  if len(lines) < 2:
53
71
  return None # Unexpected output
54
72
 
@@ -56,32 +74,32 @@ def get_vscode_commit_from_code_version() -> str | None:
56
74
  commit = lines[1].strip().decode("utf-8")
57
75
  logger.info(f"Getting commit from `code --version`: {commit}")
58
76
 
59
- return lines[1].strip().decode("utf-8")
60
-
61
-
62
- def get_target_platform_from_installer(cli_installer: str) -> str | None:
63
- directories = list(Path(cli_installer).glob("vscode-server-*.tar.gz"))
64
- if len(directories) == 1:
65
- return directories[0].name[len("vscode-server-") : -len(".tar.gz")]
66
- return None
77
+ return commit
67
78
 
68
79
 
69
80
  # Mapping from target platform to CLI OS and architecture used in download URLs
70
- _cli_os_arch_mapping = {
81
+ _cli_platform_mapping = {
71
82
  "linux-x64": "alpine-x64",
72
83
  "linux-arm64": "alpine-arm64",
84
+ "linux-armhf": "linux-arm64",
85
+ "win32-x64": "win32-x64",
73
86
  }
74
87
 
75
88
 
76
- def get_cli_os_arch(platform: str) -> str:
89
+ def get_cli_platform(platform: str) -> str:
77
90
  """Get the CLI OS and architecture for the given target platform."""
78
- if platform not in _cli_os_arch_mapping:
91
+ if platform not in _cli_platform_mapping:
79
92
  raise ValueError(f"Unsupported target platform: {platform}")
80
- return _cli_os_arch_mapping[platform]
93
+ return _cli_platform_mapping[platform]
81
94
 
82
95
 
83
96
  def get_host_platform() -> str:
84
- """Get the host platform in the format used by VS Code Server install."""
97
+ """Get the host platform in the format used by VS Code install."""
98
+ if os.name == "nt":
99
+ if "amd64" in sys.version.lower():
100
+ return "win32-x64"
101
+ raise ValueError(f"Unsupported host platform: {os.name}-{sys.version}")
102
+
85
103
  (osname, _, _, _, machine) = os.uname()
86
104
 
87
105
  if osname.lower() == "linux":
@@ -90,3 +108,21 @@ def get_host_platform() -> str:
90
108
  elif machine in ("aarch64", "arm64"):
91
109
  return "linux-arm64"
92
110
  raise ValueError(f"Unsupported host platform: {osname}-{machine}")
111
+
112
+
113
+ def get_filename_from_headers(headers: Mapping[str, str]) -> str | None:
114
+ """Get the filename from HTTP headers.
115
+
116
+ Args:
117
+ headers: The HTTP headers.
118
+ """
119
+ content_disposition = headers.get("Content-Disposition")
120
+ header_str = ""
121
+ if content_type := headers.get("Content-Type"):
122
+ header_str += f"Content-Type: {content_type}\n"
123
+ if content_disposition := headers.get("Content-Disposition"):
124
+ header_str += f"Content-Disposition: {content_disposition}\n"
125
+ if not header_str:
126
+ return None
127
+ header = HeaderParser().parsestr(header_str)
128
+ return header.get_filename()
@@ -1,25 +1,24 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: vscode-offline
3
- Version: 0.1.2
3
+ Version: 0.1.4
4
4
  Summary: Download and install VS Code Server for offline environments
5
- Author: Chuck Fan
5
+ Project-URL: Homepage, https://github.com/fanck0605/vscode-offline
6
6
  Author-email: Chuck Fan <fanck0605@qq.com>
7
7
  License-Expression: MIT
8
8
  License-File: LICENSE
9
9
  Classifier: Development Status :: 4 - Beta
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: License :: OSI Approved :: MIT License
12
+ Classifier: Operating System :: Microsoft :: Windows
13
+ Classifier: Operating System :: POSIX :: Linux
10
14
  Classifier: Programming Language :: Python :: 3 :: Only
11
15
  Classifier: Programming Language :: Python :: 3.9
12
16
  Classifier: Programming Language :: Python :: 3.10
13
17
  Classifier: Programming Language :: Python :: 3.11
14
18
  Classifier: Programming Language :: Python :: 3.12
15
19
  Classifier: Programming Language :: Python :: 3.13
16
- Classifier: Intended Audience :: Developers
17
- Classifier: License :: OSI Approved :: MIT License
18
- Classifier: Operating System :: POSIX :: Linux
19
- Classifier: Operating System :: Microsoft :: Windows
20
20
  Classifier: Topic :: Utilities
21
21
  Requires-Python: >=3.9
22
- Project-URL: Homepage, https://github.com/fanck0605/vscode-offline
23
22
  Description-Content-Type: text/markdown
24
23
 
25
24
  # vscode-offline
@@ -0,0 +1,11 @@
1
+ vscode_offline/__init__.py,sha256=dr6Jtj0XT9eQEC4fzNigEYsAIEfCsaom3HDbUsS-2O4,57
2
+ vscode_offline/app.py,sha256=uO7netdS6U-CxN77zcXwzH301dBDlwvds1GUVVMYGok,7290
3
+ vscode_offline/download.py,sha256=-6vFRoY5QKEQgIqSbx3XgzoZJz6TzLpSS8mCUygiogE,5136
4
+ vscode_offline/install.py,sha256=pIibDCbCmbidXeGUYIS6H1aAE6d1bWwZyIFJecmAASw,3545
5
+ vscode_offline/loggers.py,sha256=vX91NMtNo1xfxq5y4BCtm_uhCTKtCODqBJHNvcT7JdQ,104
6
+ vscode_offline/utils.py,sha256=Pp5W7yeT883sVkoAn5snZ2Obdqdu7wqa0vGGhLYGYmc,4000
7
+ vscode_offline-0.1.4.dist-info/METADATA,sha256=oD4uE9hkprb961ZjY6WZj0s-QMfVaOUyvbIzRA0tvp4,2474
8
+ vscode_offline-0.1.4.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
9
+ vscode_offline-0.1.4.dist-info/entry_points.txt,sha256=XyuZLe7bgm2RmZp9oh9qCxcrAwHypD8XrTnm4G0_CzM,55
10
+ vscode_offline-0.1.4.dist-info/licenses/LICENSE,sha256=pUIXFkLeTS986b7dopOVLyuw72fJsUxhl8H3rEMIycA,1053
11
+ vscode_offline-0.1.4.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.27.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -1,3 +1,2 @@
1
1
  [console_scripts]
2
2
  vscode-offline = vscode_offline:main
3
-
@@ -1,11 +0,0 @@
1
- vscode_offline/__init__.py,sha256=dr6Jtj0XT9eQEC4fzNigEYsAIEfCsaom3HDbUsS-2O4,57
2
- vscode_offline/app.py,sha256=CQbc8dFATwXV2VqSZGJAquTIe7sjae3ZGlmoQSllB-I,5666
3
- vscode_offline/download.py,sha256=R7VjdqbMZ1NLOrDF6OEhMDWCHY1MaprqALX7M_UD3Uo,3717
4
- vscode_offline/install.py,sha256=UNgLnMAAZEOtX4s8v6QlmuIHPf3aeuXrCWfC6_TEM-0,3488
5
- vscode_offline/loggers.py,sha256=vX91NMtNo1xfxq5y4BCtm_uhCTKtCODqBJHNvcT7JdQ,104
6
- vscode_offline/utils.py,sha256=tI1gktyhkRiTfBaELsNNTa6Fk4y5hyAtoebERfsyxS4,3014
7
- vscode_offline-0.1.2.dist-info/licenses/LICENSE,sha256=pUIXFkLeTS986b7dopOVLyuw72fJsUxhl8H3rEMIycA,1053
8
- vscode_offline-0.1.2.dist-info/WHEEL,sha256=-neZj6nU9KAMg2CnCY6T3w8J53nx1kFGw_9HfoSzM60,79
9
- vscode_offline-0.1.2.dist-info/entry_points.txt,sha256=zIMeh_ENKKzlt9lDao8icofSI0TeCQxH8eCwxIRI2G8,56
10
- vscode_offline-0.1.2.dist-info/METADATA,sha256=PlAToQnUc7dJrK1iuHRUajFY5Gm__yBOb4Wj2J3Gv9E,2492
11
- vscode_offline-0.1.2.dist-info/RECORD,,
@@ -1,4 +0,0 @@
1
- Wheel-Version: 1.0
2
- Generator: uv 0.8.22
3
- Root-Is-Purelib: true
4
- Tag: py3-none-any