From 3f67a87079e64978446d09cc18f1795212609b7b Mon Sep 17 00:00:00 2001 From: Nicolas KEITA Date: Mon, 26 Feb 2024 13:31:48 +0100 Subject: [PATCH 1/5] add --update option (#693) * add --update option * remove duplicate code in each frameworks about '-update' * Refactor: Explicitly set updateability for certain frameworks Set specific frameworks to be updatable and make non-updatable the default for all other frameworks * Add placeholder for tests about --update --- tests/small/test_settings.py | 22 ++++ umake/__init__.py | 2 +- umake/frameworks/__init__.py | 14 ++- umake/frameworks/android.py | 17 ++- umake/frameworks/baseinstaller.py | 127 +++++++++++++---------- umake/frameworks/dart.py | 13 ++- umake/frameworks/devops.py | 19 +++- umake/frameworks/electronics.py | 10 ++ umake/frameworks/games.py | 22 +++- umake/frameworks/go.py | 10 ++ umake/frameworks/ide.py | 165 +++++++++++++++++++++++++++++- umake/frameworks/logic.py | 1 + umake/frameworks/nodejs.py | 12 ++- umake/frameworks/rust.py | 10 ++ umake/frameworks/web.py | 40 +++++++- umake/network/download_center.py | 1 + umake/ui/cli/__init__.py | 96 +++++++++++++++++ 17 files changed, 512 insertions(+), 69 deletions(-) diff --git a/tests/small/test_settings.py b/tests/small/test_settings.py index 971f9810..20954bf9 100644 --- a/tests/small/test_settings.py +++ b/tests/small/test_settings.py @@ -20,6 +20,7 @@ """Tests the umake settings handler""" import os +import re import shutil import tempfile from ..tools import get_data_dir, LoggedTestCase @@ -97,3 +98,24 @@ def test_version_git_not_installed(self, path_join_result): path_join_result.side_effect = self.return_fake_version_path os.environ["PATH"] = "" self.assertEqual(settings.get_version(), "42.02+unknown") + + def test_get_latest_version(self): + class DartSdk: + def __init__(self): + self.package_url = 'https://storage.googleapis.com/dart-archive/channels/stable/release/3.2.4/sdk/dartsdk-linux-x64-release.zip' + self.version_regex = r'/(\d+\.\d+\.\d+)' + + def get_latest_version(self): + print(self.version_regex, self.package_url) + return (re.search(self.version_regex, self.package_url).group(1).replace('_', '.') + if self.package_url and self.version_regex else None) + + framework = DartSdk() + self.assertEqual(framework.get_latest_version(), '3.2.4') + + @patch("os.path.join") + def test_get_current_user_version(self, path_join_result): + # 1) install dart-sdk or a dummy framework and store the install_path + # 2) Initiate a framework object + # 3) assertEqual(framework.get_current_user_version(install_path), '3.2.4') + pass diff --git a/umake/__init__.py b/umake/__init__.py index 89aeb112..a3c57800 100644 --- a/umake/__init__.py +++ b/umake/__init__.py @@ -132,7 +132,7 @@ def main(): add_help=False) parser.add_argument('--help', action=_HelpAction, help=_('Show this help')) # add custom help parser.add_argument("-v", "--verbose", action="count", default=0, help=_("Increase output verbosity (2 levels)")) - + parser.add_argument('-u', '--update', action='store_true', help=_('Update installed frameworks')) parser.add_argument('-r', '--remove', action="store_true", help=_("Remove specified framework if installed")) list_group = parser.add_argument_group("List frameworks").add_mutually_exclusive_group() diff --git a/umake/frameworks/__init__.py b/umake/frameworks/__init__.py index eefd1019..11e92fca 100644 --- a/umake/frameworks/__init__.py +++ b/umake/frameworks/__init__.py @@ -30,6 +30,7 @@ import pkgutil import sys import subprocess +import re from umake.network.requirements_handler import RequirementsHandler from umake.settings import DEFAULT_INSTALL_TOOLS_PATH, UMAKE_FRAMEWORKS_ENVIRON_VARIABLE, DEFAULT_BINARY_LINK_PATH from umake.tools import ConfigHandler, NoneDict, classproperty, get_current_arch, get_current_distro_version,\ @@ -140,7 +141,8 @@ class BaseFramework(metaclass=abc.ABCMeta): def __init__(self, name, description, category, force_loading=False, logo_path=None, is_category_default=False, install_path_dir=None, only_on_archs=None, only_ubuntu=False, only_ubuntu_version=None, packages_requirements=None, only_for_removal=False, expect_license=False, - need_root_access=False, json=False, override_install_path=None): + need_root_access=False, json=False, override_install_path=None, + version_regex=None, supports_update=False): self.name = name self.description = description self.logo_path = None @@ -153,6 +155,8 @@ def __init__(self, name, description, category, force_loading=False, logo_path=N self.packages_requirements.extend(self.category.packages_requirements) self.only_for_removal = only_for_removal self.expect_license = expect_license + self.version_regex = version_regex + self.supports_update = supports_update # self.override_install_path = "" if override_install_path is None else override_install_path # don't detect anything for completion mode (as we need to be quick), so avoid opening apt cache and detect @@ -331,6 +335,14 @@ def run_for(self, args): auto_accept_license=auto_accept_license, dry_run=dry_run) + def get_latest_version(self): + return (re.search(self.version_regex, self.package_url).group(1).replace('_', '.') + if self.package_url and self.version_regex else None) + + @staticmethod + def get_current_user_version(install_path): + return None + class MainCategory(BaseCategory): diff --git a/umake/frameworks/android.py b/umake/frameworks/android.py index c810d005..a8d73837 100644 --- a/umake/frameworks/android.py +++ b/umake/frameworks/android.py @@ -19,7 +19,7 @@ """Android module""" - +import json from contextlib import suppress from gettext import gettext as _ import logging @@ -86,7 +86,10 @@ def __init__(self, **kwargs): checksum_type=ChecksumType.sha256, dir_to_decompress_in_tarball="android-studio", desktop_filename="android-studio.desktop", - required_files_path=[os.path.join("bin", "studio.sh")], **kwargs) + required_files_path=[os.path.join("bin", "studio.sh")], + version_regex=r'(\d+\.\d+)', + supports_update=True, + **kwargs) def parse_license(self, line, license_txt, in_license): """Parse Android Studio download page for license""" @@ -108,6 +111,16 @@ def post_install(self): categories="Development;IDE;", extra="StartupWMClass=jetbrains-studio")) + @staticmethod + def get_current_user_version(install_path): + try: + with open(os.path.join(install_path, 'product-info.json'), 'r') as file: + data = json.load(file) + version_not_formatted = data.get('dataDirectoryName') + return re.search(r'\d+\.\d+', version_not_formatted).group() if version_not_formatted else None + except FileNotFoundError: + return + class AndroidSDK(umake.frameworks.baseinstaller.BaseInstaller): diff --git a/umake/frameworks/baseinstaller.py b/umake/frameworks/baseinstaller.py index 3ee73fdc..518894ff 100644 --- a/umake/frameworks/baseinstaller.py +++ b/umake/frameworks/baseinstaller.py @@ -61,6 +61,7 @@ def __init__(self, *args, **kwargs): """The Downloader framework isn't instantiated directly, but is useful to inherit from for all frameworks having a set of downloads to proceed, some eventual supported_archs.""" + self.package_url = None self.download_page = kwargs["download_page"] self.checksum_type = kwargs.get("checksum_type", None) self.dir_to_decompress_in_tarball = kwargs.get("dir_to_decompress_in_tarball", "") @@ -211,6 +212,78 @@ def parse_download_link(self, line, in_download): ((url, md5sum), in_download=True/False)""" pass + def store_package_url(self, result): + logger.debug("Parse download metadata") + self.auto_accept_license = True + self.dry_run = True + + error_msg = result[self.download_page].error + if error_msg: + logger.error("An error occurred while downloading {}: {}".format(self.download_page, error_msg)) + + self.new_download_url = None + self.shasum_read_method = hasattr(self, 'get_sha_and_start_download') + with StringIO() as license_txt: + url, checksum = self.get_metadata(result, license_txt) + self.package_url = url + + def get_metadata(self, result, license_txt): + + url, checksum = (None, None) + page = result[self.download_page] + if self.json is True: + logger.debug("Using json parser") + try: + latest = json.loads(page.buffer.read().decode()) + # On a download from github, if the page is not .../releases/latest + # we want to download the latest version (beta/development) + # So we get the first element in the json tree. + # In the framework we only change the url and this condition is satisfied. + if self.download_page.startswith("https://api.github.com") and \ + not self.download_page.endswith("/latest"): + latest = latest[0] + url = None + in_download = False + (url, in_download) = self.parse_download_link(latest, in_download) + if not url: + if not self.url: + raise IndexError + else: + logger.debug("We set a temporary url while fetching the checksum") + url = self.url + except (json.JSONDecodeError, IndexError): + logger.error("Can't parse the download URL from the download page.") + UI.return_main_screen(status_code=1) + logger.debug("Found download URL: " + url) + + else: + in_license = False + in_download = False + for line in page.buffer: + line_content = line.decode() + + if self.expect_license and not self.auto_accept_license: + in_license = self.parse_license(line_content, license_txt, in_license) + + # always take the first valid (url, checksum) if not match_last_link is set to True: + download = None + # if not in_download: + if (url is None or (self.checksum_type and not checksum) or + self.match_last_link) and \ + not (self.shasum_read_method and self.new_download_url): + (download, in_download) = self.parse_download_link(line_content, in_download) + + if download is not None: + (newurl, new_checksum) = download + url = newurl if newurl is not None else url + checksum = new_checksum if new_checksum is not None else checksum + if url is not None: + if self.checksum_type and checksum: + logger.debug("Found download link for {}, checksum: {}".format(url, checksum)) + elif not self.checksum_type: + logger.debug("Found download link for {}".format(url)) + return url, checksum + @MainLoop.in_mainloop_thread def get_metadata_and_check_license(self, result): """Download files to download + license and check it""" @@ -224,59 +297,7 @@ def get_metadata_and_check_license(self, result): self.new_download_url = None self.shasum_read_method = hasattr(self, 'get_sha_and_start_download') with StringIO() as license_txt: - url, checksum = (None, None) - page = result[self.download_page] - if self.json is True: - logger.debug("Using json parser") - try: - latest = json.loads(page.buffer.read().decode()) - # On a download from github, if the page is not .../releases/latest - # we want to download the latest version (beta/development) - # So we get the first element in the json tree. - # In the framework we only change the url and this condition is satisfied. - if self.download_page.startswith("https://api.github.com") and\ - not self.download_page.endswith("/latest"): - latest = latest[0] - url = None - in_download = False - (url, in_download) = self.parse_download_link(latest, in_download) - if not url: - if not self.url: - raise IndexError - else: - logger.debug("We set a temporary url while fetching the checksum") - url = self.url - except (json.JSONDecodeError, IndexError): - logger.error("Can't parse the download URL from the download page.") - UI.return_main_screen(status_code=1) - logger.debug("Found download URL: " + url) - - else: - in_license = False - in_download = False - for line in page.buffer: - line_content = line.decode() - - if self.expect_license and not self.auto_accept_license: - in_license = self.parse_license(line_content, license_txt, in_license) - - # always take the first valid (url, checksum) if not match_last_link is set to True: - download = None - # if not in_download: - if (url is None or (self.checksum_type and not checksum) or - self.match_last_link) and\ - not(self.shasum_read_method and self.new_download_url): - (download, in_download) = self.parse_download_link(line_content, in_download) - if download is not None: - (newurl, new_checksum) = download - url = newurl if newurl is not None else url - checksum = new_checksum if new_checksum is not None else checksum - if url is not None: - if self.checksum_type and checksum: - logger.debug("Found download link for {}, checksum: {}".format(url, checksum)) - elif not self.checksum_type: - logger.debug("Found download link for {}".format(url)) - + url, checksum = self.get_metadata(result, license_txt) if hasattr(self, 'get_sha_and_start_download'): logger.debug('Run get_sha_and_start_download') DownloadCenter(urls=[DownloadItem(self.new_download_url, None)], diff --git a/umake/frameworks/dart.py b/umake/frameworks/dart.py index efc7af1c..5b4298f8 100644 --- a/umake/frameworks/dart.py +++ b/umake/frameworks/dart.py @@ -57,7 +57,10 @@ def __init__(self, **kwargs): "stable/release/latest/VERSION", dir_to_decompress_in_tarball="dart-sdk", required_files_path=[os.path.join("bin", "dart")], - json=True, **kwargs) + json=True, + version_regex=r'/(\d+\.\d+\.\d+)', + supports_update=True, + **kwargs) arch_trans = { "amd64": "x64", @@ -79,6 +82,14 @@ def post_install(self): add_env_to_user(self.name, {"PATH": {"value": os.path.join(self.install_path, "bin")}}) UI.delayed_display(DisplayMessage(self.RELOGIN_REQUIRE_MSG.format(self.name))) + @staticmethod + def get_current_user_version(install_path): + try: + with open(os.path.join(install_path, 'version'), 'r') as file: + return file.readline().strip() if file else None + except FileNotFoundError: + return + class FlutterLang(umake.frameworks.baseinstaller.BaseInstaller): diff --git a/umake/frameworks/devops.py b/umake/frameworks/devops.py index 527ffd9b..592319e9 100644 --- a/umake/frameworks/devops.py +++ b/umake/frameworks/devops.py @@ -19,7 +19,8 @@ """Devops module""" - +import re +import subprocess from gettext import gettext as _ import logging import os @@ -45,7 +46,10 @@ def __init__(self, **kwargs): download_page="https://api.github.com/repos/hashicorp/terraform/releases/latest", dir_to_decompress_in_tarball=".", required_files_path=["terraform"], - json=True, **kwargs) + json=True, + version_regex=r'/(\d+\.\d+\.\d+)', + supports_update=True, + **kwargs) arch_trans = { "amd64": "amd64", @@ -67,3 +71,14 @@ def post_install(self): """Add Terraform necessary env variables""" add_env_to_user(self.name, {"PATH": {"value": os.path.join(self.install_path)}}) UI.delayed_display(DisplayMessage(self.RELOGIN_REQUIRE_MSG.format(self.name))) + + @staticmethod + def get_current_user_version(install_path): + file = os.path.join(install_path, 'terraform') + command = f"{file} --version" + try: + result = subprocess.check_output(command, shell=True, text=True) + match = re.search(r'Terraform\s+v(\d+\.\d+\.\d+)', result) + return match.group(1) if match else None + except subprocess.CalledProcessError: + return diff --git a/umake/frameworks/electronics.py b/umake/frameworks/electronics.py index 330fca47..779572f5 100644 --- a/umake/frameworks/electronics.py +++ b/umake/frameworks/electronics.py @@ -185,6 +185,8 @@ def __init__(self, **kwargs): desktop_filename="eagle.desktop", required_files_path=["eagle"], dir_to_decompress_in_tarball="eagle-*", + version_regex=r'/(\d+(?:_\d+)*)/', + supports_update=True, **kwargs) def parse_download_link(self, line, in_download): @@ -205,6 +207,14 @@ def post_install(self): comment=self.description, categories="Development;")) + @staticmethod + def get_current_user_version(install_path): + try: + with open(os.path.join(install_path, 'bin', 'eagle.def'), 'r') as file: + return re.search(r'(\d+(\.\d+)+)', next(file)).group(1) if file else None + except FileNotFoundError: + return + class Fritzing(umake.frameworks.baseinstaller.BaseInstaller): diff --git a/umake/frameworks/games.py b/umake/frameworks/games.py index e230e5c1..4ba026db 100644 --- a/umake/frameworks/games.py +++ b/umake/frameworks/games.py @@ -62,9 +62,12 @@ def parse_download_link(self, line, in_download): """Parse Blender download links""" url = None if 'linux-x64.tar.xz' in line: - p = re.search(r'href=\"https:\/\/www\.blender\.org\/download(.*linux-x64\.tar\.xz).?"', line) - url = "https://mirrors.dotsrc.org/blender/" + p.group(1) - print(url) + p = re.search(r'href=\"(https:\/\/www\.blender\.org\/.*linux-x64\.tar\.xz).?"', line) + with suppress(AttributeError): + url = p.group(1) + filename = 'release' + re.search('blender-(.*)-linux', url).group(1).replace('.', '') + '.md5' + self.checksum_url = os.path.join(os.path.dirname(url), + filename).replace('download', 'release').replace('www', 'download') return ((url, None), in_download) def post_install(self): @@ -140,7 +143,10 @@ def __init__(self, **kwargs): dir_to_decompress_in_tarball='superpowers*', desktop_filename="superpowers.desktop", required_files_path=["Superpowers"], - json=True, **kwargs) + json=True, + version_regex='/v(\d+\.\d+\.\d+)', + supports_update=True, + **kwargs) arch_trans = { "amd64": "x64", @@ -165,6 +171,14 @@ def post_install(self): comment=self.description, categories="Development;IDE;")) + @staticmethod + def get_current_user_version(install_path): + try: + with open(os.path.join(install_path, 'version'), 'r') as file: + return file.readline().strip() if file else None + except FileNotFoundError: + return + class GDevelop(umake.frameworks.baseinstaller.BaseInstaller): diff --git a/umake/frameworks/go.py b/umake/frameworks/go.py index ad2ed43e..ee016839 100644 --- a/umake/frameworks/go.py +++ b/umake/frameworks/go.py @@ -49,6 +49,8 @@ def __init__(self, **kwargs): checksum_type=ChecksumType.sha256, dir_to_decompress_in_tarball="go", required_files_path=[os.path.join("bin", "go")], + version_regex=r'go(\d+(\.\d+)+)', + supports_update=True, **kwargs) arch_trans = { @@ -93,3 +95,11 @@ def post_install(self): add_env_to_user(self.name, {"PATH": {"value": os.path.join(self.install_path, "bin")}, "GOROOT": {"value": self.install_path, "keep": False}}) UI.delayed_display(DisplayMessage(self.RELOGIN_REQUIRE_MSG.format(self.name))) + + @staticmethod + def get_current_user_version(install_path): + try: + with open(os.path.join(install_path, 'VERSION'), 'r') as file: + return re.search(r'go(\d+(\.\d+)+)', next(file)).group(1) if file else None + except FileNotFoundError: + return diff --git a/umake/frameworks/ide.py b/umake/frameworks/ide.py index 9cd59caa..2f5571da 100644 --- a/umake/frameworks/ide.py +++ b/umake/frameworks/ide.py @@ -20,6 +20,8 @@ """Generic IDE module.""" +import json +import subprocess from abc import ABCMeta, abstractmethod from contextlib import suppress from gettext import gettext as _ @@ -276,8 +278,19 @@ def __init__(self, **kwargs): dir_to_decompress_in_tarball='pycharm-community-*', desktop_filename='jetbrains-pycharm-ce.desktop', icon_filename='pycharm.png', + version_regex=r'(\d+\.\d+)', **kwargs) + @staticmethod + def get_current_user_version(install_path): + try: + with open(os.path.join(install_path, 'product-info.json'), 'r') as file: + data = json.load(file) + version_not_formatted = data.get('dataDirectoryName') + return re.search(r'\d+\.\d+', version_not_formatted).group() if version_not_formatted else None + except FileNotFoundError: + return + class PyCharmEducational(BaseJetBrains): """The JetBrains PyCharm Educational Edition distribution.""" @@ -292,9 +305,21 @@ def __init__(self, **kwargs): dir_to_decompress_in_tarball='pycharm-edu*', desktop_filename='jetbrains-pycharm-edu.desktop', icon_filename='pycharm.png', + version_regex=r'(\d+\.\d+)', **kwargs) + @staticmethod + def get_current_user_version(install_path): + try: + with open(os.path.join(install_path, 'product-info.json'), 'r') as file: + data = json.load(file) + version_not_formatted = data.get('dataDirectoryName') + return re.search(r'\d+\.\d+', version_not_formatted).group() if version_not_formatted else None + except FileNotFoundError: + return + + class PyCharmProfessional(BaseJetBrains): """The JetBrains PyCharm Professional Edition distribution.""" download_keyword = 'PCP' @@ -308,9 +333,21 @@ def __init__(self, **kwargs): dir_to_decompress_in_tarball='pycharm-*', desktop_filename='jetbrains-pycharm.desktop', icon_filename='pycharm.png', + version_regex=r'(\d+\.\d+)', **kwargs) + @staticmethod + def get_current_user_version(install_path): + try: + with open(os.path.join(install_path, 'product-info.json'), 'r') as file: + data = json.load(file) + version_not_formatted = data.get('dataDirectoryName') + return re.search(r'\d+\.\d+', version_not_formatted).group() if version_not_formatted else None + except FileNotFoundError: + return + + class Idea(BaseJetBrains): """The JetBrains IntelliJ Idea Community Edition distribution.""" download_keyword = 'IIC' @@ -323,8 +360,19 @@ def __init__(self, **kwargs): dir_to_decompress_in_tarball='idea-IC-*', desktop_filename='jetbrains-idea-ce.desktop', icon_filename='idea.png', + version_regex=r'(\d+\.\d+)', **kwargs) + @staticmethod + def get_current_user_version(install_path): + try: + with open(os.path.join(install_path, 'product-info.json'), 'r') as file: + data = json.load(file) + version_not_formatted = data.get('dataDirectoryName') + return re.search(r'\d+\.\d+', version_not_formatted).group() if version_not_formatted else None + except FileNotFoundError: + return + class IdeaUltimate(BaseJetBrains): """The JetBrains IntelliJ Idea Ultimate Edition distribution.""" @@ -338,8 +386,19 @@ def __init__(self, **kwargs): dir_to_decompress_in_tarball='idea-IU-*', desktop_filename='jetbrains-idea.desktop', icon_filename='idea.png', + version_regex=r'(\d+\.\d+)', **kwargs) + @staticmethod + def get_current_user_version(install_path): + try: + with open(os.path.join(install_path, 'product-info.json'), 'r') as file: + data = json.load(file) + version_not_formatted = data.get('dataDirectoryName') + return re.search(r'\d+\.\d+', version_not_formatted).group() if version_not_formatted else None + except FileNotFoundError: + return + class RubyMine(BaseJetBrains): """The JetBrains RubyMine IDE""" @@ -369,8 +428,18 @@ def __init__(self, **kwargs): dir_to_decompress_in_tarball='WebStorm-*', desktop_filename='jetbrains-webstorm.desktop', icon_filename='webstorm.svg', + version_regex=r'WebStorm-(\d+(\.\d+)+)', **kwargs) + @staticmethod + def get_current_user_version(install_path): + try: + with open(os.path.join(install_path, 'product-info.json'), 'r') as file: + data = json.load(file) + return data.get('version') + except FileNotFoundError: + return + class PhpStorm(BaseJetBrains): """The JetBrains PhpStorm IDE""" @@ -384,8 +453,18 @@ def __init__(self, **kwargs): dir_to_decompress_in_tarball='PhpStorm-*', desktop_filename='jetbrains-phpstorm.desktop', icon_filename='phpstorm.png', + version_regex=r'-(\d+\.\d+\.\d+)', **kwargs) + @staticmethod + def get_current_user_version(install_path): + try: + with open(os.path.join(install_path, 'product-info.json'), 'r') as file: + data = json.load(file) + return data.get('version') + except FileNotFoundError: + return + class CLion(BaseJetBrains): """The JetBrains CLion IDE""" @@ -399,8 +478,18 @@ def __init__(self, **kwargs): dir_to_decompress_in_tarball='clion-*', desktop_filename='jetbrains-clion.desktop', icon_filename='clion.svg', + version_regex=r'-(\d+\.\d+\.\d+)', **kwargs) + @staticmethod + def get_current_user_version(install_path): + try: + with open(os.path.join(install_path, 'product-info.json'), 'r') as file: + data = json.load(file) + return data.get('version') + except FileNotFoundError: + return + class DataGrip(BaseJetBrains): """The JetBrains DataGrip IDE""" @@ -414,8 +503,18 @@ def __init__(self, **kwargs): dir_to_decompress_in_tarball='DataGrip-*', desktop_filename='jetbrains-datagrip.desktop', icon_filename='datagrip.png', + version_regex=r'-(\d+\.\d+\.\d+)', **kwargs) + @staticmethod + def get_current_user_version(install_path): + try: + with open(os.path.join(install_path, 'product-info.json'), 'r') as file: + data = json.load(file) + return data.get('version') + except FileNotFoundError: + return + class GoLand(BaseJetBrains): """The JetBrains GoLand IDE""" @@ -429,8 +528,18 @@ def __init__(self, **kwargs): dir_to_decompress_in_tarball='GoLand-*', desktop_filename='jetbrains-goland.desktop', icon_filename='goland.png', + version_regex=r'-(\d+\.\d+\.\d+)', **kwargs) + @staticmethod + def get_current_user_version(install_path): + try: + with open(os.path.join(install_path, 'product-info.json'), 'r') as file: + data = json.load(file) + return data.get('version') + except FileNotFoundError: + return + class Rider(BaseJetBrains): """The JetBrains cross-platform .NET IDE""" @@ -572,7 +681,10 @@ def __init__(self, **kwargs): desktop_filename="lighttable.desktop", required_files_path=["LightTable"], dir_to_decompress_in_tarball="lighttable-*", - json=True, **kwargs) + json=True, + version_regex=r'(\d+\.\d+)', + supports_update=True, + **kwargs) def parse_download_link(self, line, in_download): url = None @@ -592,6 +704,15 @@ def post_install(self): comment=_("LightTable code editor"), categories="Development;IDE;")) + @staticmethod + def get_current_user_version(install_path): + try: + with open(os.path.join(install_path, 'resources', 'app', 'package.json'), 'r') as file: + data = json.load(file) + return data.get('version') + except FileNotFoundError: + return + class Atom(umake.frameworks.baseinstaller.BaseInstaller): @@ -643,6 +764,8 @@ def __init__(self, **kwargs): desktop_filename="sublime-text.desktop", required_files_path=["sublime_text"], dir_to_decompress_in_tarball="sublime_text", + version_regex=r'_build_(\d+)', + supports_update=True, **kwargs) arch_trans = { @@ -668,6 +791,16 @@ def post_install(self): comment=_("Sophisticated text editor for code, markup and prose"), categories="Development;TextEditor;")) + @staticmethod + def get_current_user_version(install_path): + try: + command = f"{os.path.join(install_path, 'sublime_text')} --version" + result = subprocess.check_output(command, shell=True, text=True) + match = re.search(r'(\d+)', result) + return match.group(1) if match else None + except subprocess.CalledProcessError: + return + class SpringToolsSuite(umake.frameworks.baseinstaller.BaseInstaller): def __init__(self, **kwargs): @@ -718,7 +851,10 @@ def __init__(self, **kwargs): desktop_filename="processing.desktop", required_files_path=["processing"], dir_to_decompress_in_tarball="processing-*", - json=True, **kwargs) + json=True, + version_regex=r'(\d+\.\d+)', + supports_update=True, + **kwargs) arch_trans = { "amd64": "64", @@ -742,6 +878,16 @@ def post_install(self): comment=_("Processing is a flexible software sketchbook"), categories="Development;IDE;")) + @staticmethod + def get_current_user_version(install_path): + try: + with open(os.path.join(install_path, 'revisions.txt'), 'r') as file: + first_line = file.readline().strip() + match = re.search(r'(\d+\.\d+\.\d+)', first_line) + return match.group(1) if match else None + except FileNotFoundError: + return + class LiteIDE(umake.frameworks.baseinstaller.BaseInstaller): @@ -753,7 +899,10 @@ def __init__(self, **kwargs): desktop_filename="liteide.desktop", required_files_path=["bin/liteide"], dir_to_decompress_in_tarball="liteide", - json=True, **kwargs) + json=True, + version_regex=r'(\d+\.\d+)', + supports_update=True, + **kwargs) arch_trans = { "amd64": "64", @@ -778,6 +927,16 @@ def post_install(self): comment=_("LiteIDE is a simple, open source, cross-platform Go IDE."), categories="Development;IDE;")) + @staticmethod + def get_current_user_version(install_path): + try: + with open(os.path.join(install_path, 'README.md'), 'r') as file: + content = ''.join(file.readline() for _ in range(15)) + match = re.search(r'(\d+\.\d+)', content) + return match.group(1) if match else None + except FileNotFoundError: + return + class RStudio(umake.frameworks.baseinstaller.BaseInstaller): diff --git a/umake/frameworks/logic.py b/umake/frameworks/logic.py index 87e2fcd1..edca9b35 100644 --- a/umake/frameworks/logic.py +++ b/umake/frameworks/logic.py @@ -47,6 +47,7 @@ def __init__(self, **kwargs): dir_to_decompress_in_tarball="Protege-*", required_files_path=["protege"], desktop_filename="protege.desktop", + version_regex=r'/protege-(\d+\.\d+\.\d+)/', json=True, **kwargs) def parse_download_link(self, line, in_download): diff --git a/umake/frameworks/nodejs.py b/umake/frameworks/nodejs.py index 323dd584..2eac8cd7 100644 --- a/umake/frameworks/nodejs.py +++ b/umake/frameworks/nodejs.py @@ -19,7 +19,7 @@ """Nodejs module""" - +import subprocess from contextlib import suppress from gettext import gettext as _ import logging @@ -126,3 +126,13 @@ def run_for(self, args): if not args.remove: print('Download from {}'.format(self.download_page)) super().run_for(args) + + @staticmethod + def get_current_user_version(install_path): + try: + command = f"{os.path.join(install_path, 'bin', 'node')} --version" + result = subprocess.check_output(command, shell=True, text=True) + match = re.search(r'v(\d+\.\d+\.\d+)', result) + return match.group(1) if match else None + except subprocess.CalledProcessError: + return diff --git a/umake/frameworks/rust.py b/umake/frameworks/rust.py index f7fa4e58..cbb86487 100644 --- a/umake/frameworks/rust.py +++ b/umake/frameworks/rust.py @@ -49,6 +49,8 @@ def __init__(self, **kwargs): only_on_archs=['i386', 'amd64'], download_page="https://www.rust-lang.org/en-US/other-installers.html", dir_to_decompress_in_tarball="rust-*", + version_regex=r'rust-(\d+(\.\d+)+)', + supports_update=True, **kwargs) arch_trans = { "amd64": "x86_64", @@ -82,3 +84,11 @@ def post_install(self): os.path.join(arch_dest_lib_folder, f)) UI.delayed_display(DisplayMessage(self.RELOGIN_REQUIRE_MSG.format(self.name))) + + @staticmethod + def get_current_user_version(install_path): + try: + with open(os.path.join(install_path, 'version'), 'r') as file: + return re.search(r'(\d+(\.\d+)+)', next(file)).group(1) if file else None + except FileNotFoundError: + return diff --git a/umake/frameworks/web.py b/umake/frameworks/web.py index cd8e0907..af5ac3f0 100644 --- a/umake/frameworks/web.py +++ b/umake/frameworks/web.py @@ -19,7 +19,7 @@ """Web module""" - +import subprocess from contextlib import suppress from functools import partial from gettext import gettext as _ @@ -150,6 +150,8 @@ def __init__(self, **kwargs): download_page="http://phantomjs.org/download.html", dir_to_decompress_in_tarball="phantomjs*", required_files_path=[os.path.join("bin", "phantomjs")], + version_regex=r'(\d+\.\d+)', + supports_update=True, **kwargs) arch_trans = { @@ -177,6 +179,16 @@ def post_install(self): add_env_to_user(self.name, {"PATH": {"value": os.path.join(self.install_path, "bin")}}) UI.delayed_display(DisplayMessage(self.RELOGIN_REQUIRE_MSG.format(self.name))) + @staticmethod + def get_current_user_version(install_path): + try: + with open(os.path.join(install_path, 'ChangeLog'), 'r') as file: + lines = ''.join(file.readline() for _ in range(3)) + match = re.search(r'(\d+\.\d+)', lines) + return match.group(1) if match else None + except FileNotFoundError: + return + class Geckodriver(umake.frameworks.baseinstaller.BaseInstaller): @@ -188,6 +200,8 @@ def __init__(self, **kwargs): download_page="https://api.github.com/repos/mozilla/geckodriver/releases/latest", dir_to_decompress_in_tarball=".", required_files_path=["geckodriver"], + version_regex=r'v(\d+\.\d+\.\d+)', + supports_update=True, json=True, **kwargs) arch_trans = { @@ -209,6 +223,18 @@ def post_install(self): add_env_to_user(self.name, {"PATH": {"value": os.path.join(self.install_path)}}) UI.delayed_display(DisplayMessage(self.RELOGIN_REQUIRE_MSG.format(self.name))) + @staticmethod + def get_current_user_version(install_path): + try: + command = f"{os.path.join(install_path, 'geckodriver')} --version" + result = subprocess.check_output(command, shell=True, text=True) + first_line = result.split('\n')[0] + match = re.search(r'geckodriver\s+(\d+\.\d+\.\d+)', first_line) + return match.group(1) if match else None + except subprocess.CalledProcessError: + return + + class Chromedriver(umake.frameworks.baseinstaller.BaseInstaller): @@ -218,6 +244,8 @@ def __init__(self, **kwargs): download_page="https://chromedriver.storage.googleapis.com/LATEST_RELEASE", dir_to_decompress_in_tarball=".", required_files_path=["chromedriver"], + version_regex=r'/(\d+\.\d+\.\d+\.\d+)', + supports_update=True, **kwargs) def parse_download_link(self, line, in_download): @@ -231,3 +259,13 @@ def post_install(self): """Add Chromedriver necessary env variables""" add_env_to_user(self.name, {"PATH": {"value": os.path.join(self.install_path)}}) UI.delayed_display(DisplayMessage(self.RELOGIN_REQUIRE_MSG.format(self.name))) + + @staticmethod + def get_current_user_version(install_path): + try: + command = f"{os.path.join(install_path, 'chromedriver')} --version" + result = subprocess.check_output(command, shell=True, text=True) + match = re.search(r'ChromeDriver\s+([\d.]+)', result) + return match.group(1) if match else None + except subprocess.CalledProcessError: + return diff --git a/umake/network/download_center.py b/umake/network/download_center.py index 4b4dd8fa..dfa5de36 100644 --- a/umake/network/download_center.py +++ b/umake/network/download_center.py @@ -206,6 +206,7 @@ def _done(self): """ logger.info("All pending downloads for {} done".format(self._urls)) self._done_callback(self._downloaded_content) + self._wired_report('all downloads finished') @classmethod def _checksum_for_fd(cls, algorithm, f, block_size=2 ** 20): diff --git a/umake/ui/cli/__init__.py b/umake/ui/cli/__init__.py index f28fa23d..d617b2e2 100644 --- a/umake/ui/cli/__init__.py +++ b/umake/ui/cli/__init__.py @@ -19,6 +19,7 @@ """Module for loading the command line interface""" +import threading import argcomplete from contextlib import suppress from gettext import gettext as _ @@ -28,6 +29,7 @@ import readline import sys from umake.interactions import InputText, TextWithChoices, LicenseAgreement, DisplayMessage, UnknownProgress +from umake.network.download_center import DownloadItem, DownloadCenter from umake.ui import UI from umake.frameworks import BaseCategory, list_frameworks from umake.tools import InputError, MainLoop @@ -220,6 +222,53 @@ def get_frameworks_list_output(args): return print_result +def is_first_version_higher(version1, version2): + if version2 is None: + return True + elif version1 is None: + return False + + v1_parts = list(map(int, version1.split('.'))) + v2_parts = list(map(int, version2.split('.'))) + for v1, v2 in zip(v1_parts, v2_parts): + if v1 > v2: + return True + elif v1 < v2: + return False + return len(v1_parts) > len(v2_parts) + + +def pretty_print_versions(data): + max_name_length = max(len(item['framework_name']) for item in data) + max_version_length = max(len(item['latest_version']) for item in data) + supports_color = os.getenv('TERM') and os.getenv('TERM') != 'dumb' + + reset_color = '' + if supports_color: + reset_color = '\033[m' + + for item in data: + latest_version = item['latest_version'] + user_version = item['user_version'] + latest_version_color = '' + user_version_color = '' + symbol = '+' + if supports_color: + latest_version_color = '\033[32m' + user_version_color = '\033[31m' + + latest_version_formatted = f"{latest_version_color}{latest_version}{reset_color}" + latest_version_padding = len(latest_version_formatted) + latest_version_formatted = latest_version_formatted.ljust( + latest_version_padding - + len(latest_version) + + max_version_length + ) + print(f"{item['framework_name'].ljust(max_name_length)} | " + f"Latest Version: {latest_version_formatted} | " + f"User Version: {user_version_color}{user_version} {symbol}{reset_color}") + + def main(parser): """Main entry point of the cli command""" categories_parser = parser.add_subparsers(help='Developer environment', dest="category") @@ -243,6 +292,53 @@ def main(parser): print(get_version()) sys.exit(0) + if args.update: + frameworks = list_frameworks() + installed_frameworks = sorted([ + {'framework_name': framework['framework_name'], + 'install_path': framework['install_path'], + 'category_name': category['category_name']} + for category in frameworks + for framework in category['frameworks'] if framework['is_installed'] + ], key=lambda x: x['framework_name']) + outdated_frameworks = [] + for installed_framework in installed_frameworks: + category_name = installed_framework['category_name'] + framework_name = installed_framework['framework_name'] + if category_name == 'java' or framework_name == 'firefox-dev': + continue + install_path = installed_framework['install_path'] + framework = BaseCategory.categories[category_name].frameworks[framework_name] + if framework.supports_update: + fetch_package_url = threading.Event() + DownloadCenter([DownloadItem(framework.download_page)], framework.store_package_url, + download=False, + report=lambda arg: fetch_package_url.set() if arg == 'all downloads finished' else None) + fetch_package_url.wait() + user_version = framework.get_current_user_version(install_path) + latest_version = framework.get_latest_version() + is_outdated = is_first_version_higher(latest_version, user_version) \ + if (latest_version is not None and user_version is not None) else False + if is_outdated: + outdated_frameworks.append({ + 'framework_name': framework_name, + 'category_name': category_name, + 'user_version': user_version, + 'latest_version': latest_version, + 'is_outdated': is_outdated, + }) + if len(outdated_frameworks) == 0: + print('All packages are up-to-date.') + sys.exit(0) + else: + pretty_print_versions(outdated_frameworks) + for outdated_framework in outdated_frameworks: + args = parser.parse_args([outdated_framework['category_name'], outdated_framework['framework_name']]) + CliUI() + run_command_for_args(args) + return + sys.exit(0) + if not args.category: parser.print_help() sys.exit(0) From 2584b1ae4220754393b1098b0fe27df9e0cc8333 Mon Sep 17 00:00:00 2001 From: Jeffrey04 Date: Thu, 28 Mar 2024 01:43:21 +0800 Subject: [PATCH 2/5] update godot logo URL --- umake/frameworks/games.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/umake/frameworks/games.py b/umake/frameworks/games.py index e230e5c1..9d591272 100644 --- a/umake/frameworks/games.py +++ b/umake/frameworks/games.py @@ -182,7 +182,7 @@ def __init__(self, **kwargs): desktop_filename="godot.desktop", required_files_path=['godot'], **kwargs) - self.icon_url = "https://godotengine.org/themes/godotengine/assets/download/godot_logo.svg" + self.icon_url = "https://godotengine.org/assets/press/icon_color.svg" self.icon_filename = "Godot.svg" arch_trans = { From 95076c98b3fd22f80430e8dfca79ffe3197e971a Mon Sep 17 00:00:00 2001 From: Galileo Sartor Date: Sat, 27 Apr 2024 12:19:00 +0100 Subject: [PATCH 3/5] Add assume-yes to skip interaction on update --- umake/__init__.py | 1 + umake/frameworks/__init__.py | 6 +++++- umake/frameworks/baseinstaller.py | 14 +++++++++++--- umake/ui/cli/__init__.py | 5 +++++ 4 files changed, 22 insertions(+), 4 deletions(-) diff --git a/umake/__init__.py b/umake/__init__.py index a3c57800..756580a3 100644 --- a/umake/__init__.py +++ b/umake/__init__.py @@ -133,6 +133,7 @@ def main(): parser.add_argument('--help', action=_HelpAction, help=_('Show this help')) # add custom help parser.add_argument("-v", "--verbose", action="count", default=0, help=_("Increase output verbosity (2 levels)")) parser.add_argument('-u', '--update', action='store_true', help=_('Update installed frameworks')) + parser.add_argument('-y', '--assume-yes', action='store_true', help=_('Assume yes at interactive prompts')) parser.add_argument('-r', '--remove', action="store_true", help=_("Remove specified framework if installed")) list_group = parser.add_argument_group("List frameworks").add_mutually_exclusive_group() diff --git a/umake/frameworks/__init__.py b/umake/frameworks/__init__.py index 11e92fca..ea840b6f 100644 --- a/umake/frameworks/__init__.py +++ b/umake/frameworks/__init__.py @@ -325,15 +325,19 @@ def run_for(self, args): install_path = None auto_accept_license = False dry_run = False + assume_yes = False if args.destdir: install_path = os.path.abspath(os.path.expanduser(args.destdir)) if self.expect_license and args.accept_license: auto_accept_license = True if args.dry_run: dry_run = True + if args.assume_yes: + assume_yes = True self.setup(install_path=install_path, auto_accept_license=auto_accept_license, - dry_run=dry_run) + dry_run=dry_run, + assume_yes=assume_yes) def get_latest_version(self): return (re.search(self.version_regex, self.package_url).group(1).replace('_', '.') diff --git a/umake/frameworks/baseinstaller.py b/umake/frameworks/baseinstaller.py index 518894ff..d6750434 100644 --- a/umake/frameworks/baseinstaller.py +++ b/umake/frameworks/baseinstaller.py @@ -103,18 +103,22 @@ def is_installed(self): logger.debug("{} is installed".format(self.name)) return True - def setup(self, install_path=None, auto_accept_license=False, dry_run=False): + def setup(self, install_path=None, auto_accept_license=False, dry_run=False, assume_yes=False): self.arg_install_path = install_path self.auto_accept_license = auto_accept_license self.dry_run = dry_run + self.assume_yes = assume_yes super().setup() # first step, check if installed or dry_run if self.dry_run: self.download_provider_page() elif self.is_installed: - UI.display(YesNo("{} is already installed on your system, do you want to reinstall " - "it anyway?".format(self.name), self.reinstall, UI.return_main_screen)) + if self.assume_yes: + self.reinstall() + else: + UI.display(YesNo("{} is already installed on your system, do you want to reinstall " + "it anyway?".format(self.name), self.reinstall, UI.return_main_screen)) else: self.confirm_path(self.arg_install_path) @@ -163,6 +167,10 @@ def set_exec_path(self): def confirm_path(self, path_dir=""): """Confirm path dir""" + if self.assume_yes: + UI.display(DisplayMessage("Assuming default path: " + self.install_path)) + path_dir = self.install_path + if not path_dir: logger.debug("No installation path provided. Requesting one.") UI.display(InputText("Choose installation path:", self.confirm_path, self.install_path)) diff --git a/umake/ui/cli/__init__.py b/umake/ui/cli/__init__.py index d617b2e2..6fa744f6 100644 --- a/umake/ui/cli/__init__.py +++ b/umake/ui/cli/__init__.py @@ -120,6 +120,9 @@ def mangle_args_for_default_framework(args): if not category_name and arg in ("--remove", "-r"): args_to_append.append(arg) continue + if arg in ("--assume-yes", '-y'): + args_to_append.append(arg) + continue if not arg.startswith('-') and not skip_all: if not category_name: if arg in BaseCategory.categories.keys(): @@ -283,6 +286,7 @@ def main(parser): # manipulate sys.argv for default frameworks: arg_to_parse = mangle_args_for_default_framework(arg_to_parse) args = parser.parse_args(arg_to_parse) + assume_yes = args.assume_yes if args.list or args.list_installed or args.list_available: print(get_frameworks_list_output(args)) @@ -334,6 +338,7 @@ def main(parser): pretty_print_versions(outdated_frameworks) for outdated_framework in outdated_frameworks: args = parser.parse_args([outdated_framework['category_name'], outdated_framework['framework_name']]) + args.assume_yes = assume_yes CliUI() run_command_for_args(args) return From 12f41a34bc455edca6e9309040ef20dd2a77c30d Mon Sep 17 00:00:00 2001 From: Jeffrey04 Date: Thu, 13 Jun 2024 23:58:48 +0800 Subject: [PATCH 4/5] remove obsolete requirement --- umake/frameworks/ide.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/umake/frameworks/ide.py b/umake/frameworks/ide.py index 9cd59caa..9b01febf 100644 --- a/umake/frameworks/ide.py +++ b/umake/frameworks/ide.py @@ -512,7 +512,7 @@ def __init__(self, **kwargs): desktop_filename="visual-studio-code.desktop", required_files_path=["bin/code"], dir_to_decompress_in_tarball="VSCode-linux-*", - packages_requirements=["libgtk2.0-0", "libgconf-2-4"], + packages_requirements=["libgtk2.0-0"], **kwargs) def parse_license(self, line, license_txt, in_license): From 5667deab380b868b5675319225ade3fa2b39c0d7 Mon Sep 17 00:00:00 2001 From: Peter Dave Hello Date: Wed, 26 Jun 2024 17:09:27 +0000 Subject: [PATCH 5/5] Translated using Weblate (Chinese (Traditional)) Currently translated at 98.7% (77 of 78 strings) Co-authored-by: Peter Dave Hello Translate-URL: https://hosted.weblate.org/projects/ubuntu-make/ubuntu-make/zh_Hant/ Translation: Ubuntu-make/Ubuntu-make --- po/zh_TW.po | 99 +++++++++++++++++++++++------------------------------ 1 file changed, 43 insertions(+), 56 deletions(-) diff --git a/po/zh_TW.po b/po/zh_TW.po index 5d6bdf78..a6cddd8b 100644 --- a/po/zh_TW.po +++ b/po/zh_TW.po @@ -8,8 +8,8 @@ msgstr "" "Project-Id-Version: ubuntu-make\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2015-11-17 09:26+0100\n" -"PO-Revision-Date: 2023-12-14 08:05+0000\n" -"Last-Translator: reimu105 \n" +"PO-Revision-Date: 2024-06-26 17:09+0000\n" +"Last-Translator: Peter Dave Hello \n" "Language-Team: Chinese (Traditional) \n" "Language: zh_TW\n" @@ -17,7 +17,7 @@ msgstr "" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=1; plural=0;\n" -"X-Generator: Weblate 5.3-rc\n" +"X-Generator: Weblate 5.6-rc\n" "X-Launchpad-Export-Date: 2015-05-04 07:08+0000\n" #: umake/tools.py:48 @@ -30,16 +30,14 @@ msgstr "* 命令「{}」:" #: umake/__init__.py:107 msgid "Deploy and setup developers environment easily on ubuntu" -msgstr "在 Ubuntu 上輕易地佈署與安裝開發環境" +msgstr "在 Ubuntu 上輕鬆佈署與設置開發者環境" #: umake/__init__.py:108 -#, fuzzy msgid "" "Note that you can also configure different debug logging behavior using " "LOG_CFG that points to a log yaml profile." -msgstr "" -"注意您也可以使用 LOG_CFG 環境變數指向一個 YAML 格式除錯紀錄設定檔來設定不同的" -"除錯紀錄行為。" +msgstr "注意,您也可以使用 LOG_CFG 環境變數指向一個 YAML " +"格式的日誌設定檔來配置不同的除錯日誌行為。" #: umake/__init__.py:111 msgid "Show this help" @@ -47,11 +45,11 @@ msgstr "顯示此幫助訊息" #: umake/__init__.py:112 msgid "Increase output verbosity (2 levels)" -msgstr "增加輸出訊息的冗長程度(最多可提高 2 級)" +msgstr "增加輸出訊息的詳細程度(最多可提高 2 級)" #: umake/__init__.py:114 msgid "Remove specified framework if installed" -msgstr "如果已被安裝的話移除指定的軟體框架" +msgstr "移除已安裝的指定框架" #: umake/__init__.py:116 msgid "Print version and exit" @@ -59,37 +57,35 @@ msgstr "顯示版本並離開" #: umake/frameworks/ide.py:63 msgid "Generic IDEs" -msgstr "一般整合式開發環境" +msgstr "通用整合式開發環境 (IDE)" #: umake/frameworks/ide.py:76 msgid "Pure Eclipse Luna (4.4)" -msgstr "純粹的 Eclipse Luna (4.4) 整合式開發環境" +msgstr "純粹的 Eclipse Luna (4.4) 整合式開發環境 (IDE)" #: umake/frameworks/ide.py:128 msgid "The Eclipse Luna Integrated Development Environment" -msgstr "Eclipse Luna 整合式開發環境" +msgstr "Eclipse Luna 整合式開發環境 (IDE)" #: umake/frameworks/ide.py:131 msgid "Eclipse Luna" -msgstr "Eclipse Luna 整合式開發環境" +msgstr "Eclipse Luna 整合式開發環境 (IDE)" #: umake/frameworks/ide.py:217 msgid "PyCharm Community Edition" -msgstr "PyCharm 社群版本" +msgstr "PyCharm 社群版" #: umake/frameworks/ide.py:233 -#, fuzzy msgid "PyCharm Educational Edition" -msgstr "PyCharm 社群版本" +msgstr "PyCharm 教育版" #: umake/frameworks/ide.py:249 -#, fuzzy msgid "PyCharm Professional Edition" -msgstr "PyCharm 社群版本" +msgstr "PyCharm 專業版" #: umake/frameworks/ide.py:265 msgid "IntelliJ IDEA Community Edition" -msgstr "IntelliJ IDEA 社群版本" +msgstr "IntelliJ IDEA 社群版" #: umake/frameworks/ide.py:281 msgid "IntelliJ IDEA" @@ -97,20 +93,19 @@ msgstr "IntelliJ IDEA" #: umake/frameworks/ide.py:297 msgid "Ruby on Rails IDE" -msgstr "Rails IDE 上的 Ruby" +msgstr "Rails on Ruby 整合式開發環境 (IDE)" #: umake/frameworks/ide.py:314 msgid "Complex client-side and server-side javascript IDE" -msgstr "複雜的客戶端和伺服器端 JavaScript IDE" +msgstr "複雜的客戶端和伺服器端 JavaScript 整合式開發環境 (IDE)" #: umake/frameworks/ide.py:331 -#, fuzzy msgid "PHP and web development IDE" -msgstr "Stencyl 遊戲開發者整合式開發環境" +msgstr "PHP 和網頁整合式開發環境 (IDE)" #: umake/frameworks/ide.py:348 msgid "CLion integrated C/C++ IDE" -msgstr "CLion 整合 C/C++ IDE" +msgstr "CLion 整合 C/C++ 整合式開發環境 (IDE)" #: umake/frameworks/ide.py:376 msgid "The Arduino Software Distribution" @@ -118,35 +113,32 @@ msgstr "Arduino 軟體發行版" #: umake/frameworks/ide.py:475 msgid "The Arduino Software IDE" -msgstr "Arduino 軟體 IDE" +msgstr "Arduino 軟體整合式開發環境 (IDE)" #: umake/frameworks/ide.py:478 msgid "Arduino" msgstr "Arduino" #: umake/frameworks/ide.py:484 -#, fuzzy msgid "You need to logout and login again for your installation to work" -msgstr "您必須重新啟動殼程式(shell)工作階段您的安裝才會生效" +msgstr "您需要登出並重新登入,您的安裝才會生效" #: umake/frameworks/ide.py:504 umake/frameworks/ide.py:584 #: umake/frameworks/ide.py:587 msgid "Netbeans IDE" -msgstr "Netbeans IDE" +msgstr "Netbeans 整合式開發環境 (IDE)" #: umake/frameworks/dart.py:43 msgid "Dartlang Development Environment" msgstr "Dartlang 開發環境" #: umake/frameworks/dart.py:49 -#, fuzzy msgid "Dart SDK with editor (not supported upstream anyymore)" -msgstr "Dart 軟體開發工具(SDK)與編輯器(預設)" +msgstr "Dart 軟體開發工具包 (SDK) 及編輯器(不再支援)" #: umake/frameworks/dart.py:56 -#, fuzzy msgid "Dart SDK (default)" -msgstr "Dart 軟體開發工具(SDK)與編輯器(預設)" +msgstr "Dart 軟體開發工具包 (SDK)(預設)" #: umake/frameworks/dart.py:97 umake/frameworks/rust.py:134 #: umake/frameworks/scala.py:66 umake/frameworks/go.py:76 @@ -158,22 +150,20 @@ msgid "" msgstr "您必須重新啟動殼程式(shell)工作階段您的安裝才會生效" #: umake/frameworks/rust.py:43 -#, fuzzy msgid "Rust language" -msgstr "Go 程式語言" +msgstr "Rust 程式語言" #: umake/frameworks/rust.py:56 msgid "The official Rust distribution" -msgstr "Rust 官方發行" +msgstr "Rust 官方發行版" #: umake/frameworks/scala.py:40 msgid "The Scala Programming Language" msgstr "Scala 程式語言" #: umake/frameworks/scala.py:46 -#, fuzzy msgid "Scala compiler and interpreter (default)" -msgstr "Google 編譯器(預設)" +msgstr "Scala 編譯器和直譯器(預設)" #: umake/frameworks/go.py:39 msgid "Go language" @@ -200,13 +190,12 @@ msgid "Android Studio developer environment" msgstr "Android Studio 軟體開發環境" #: umake/frameworks/android.py:111 -#, fuzzy msgid "Android SDK" -msgstr "Android NDK" +msgstr "Android 軟體開發工具包 (SDK)" #: umake/frameworks/android.py:147 msgid "Android NDK" -msgstr "Android NDK" +msgstr "Android 原生開發工具包 (NDK)" #: umake/frameworks/__init__.py:129 msgid "A default framework for category {} was requested where there is none" @@ -218,16 +207,16 @@ msgstr "您無法在這台機器上安裝該框架" #: umake/frameworks/__init__.py:256 msgid "You can't remove {} as it isn't installed" -msgstr "您不能移除 {} 因為它沒有被安裝" +msgstr "您不能移除 {},因為它沒有被安裝" #: umake/frameworks/__init__.py:285 msgid "" "If the default framework name isn't provided, destdir should contain a /" -msgstr "如未提供預設框架名稱,輸出目錄應包含斜線 /" +msgstr "如果未提供預設框架名稱,則輸出目錄應包含斜線 /" #: umake/frameworks/__init__.py:288 msgid "Remove framework if installed" -msgstr "如果軟體框架已經被安裝的話移除它" +msgstr "移除已安裝的框架" #: umake/frameworks/__init__.py:291 msgid "Accept license without prompting" @@ -235,16 +224,15 @@ msgstr "接受許可證而不提示" #: umake/frameworks/web.py:45 msgid "Web Developer Environment" -msgstr "Web 開發環境" +msgstr "網頁開發環境" #: umake/frameworks/web.py:51 umake/frameworks/web.py:120 msgid "Firefox Developer Edition" -msgstr "Firefox 開發版" +msgstr "Firefox 開發者版" #: umake/frameworks/web.py:116 -#, fuzzy msgid "Choose language: {}" -msgstr "Go 程式語言" +msgstr "選擇語言:{}" #: umake/frameworks/web.py:123 msgid "Firefox Aurora with Developer tools" @@ -256,7 +244,7 @@ msgstr "在沒有提示的情況下以指定的語言安裝" #: umake/frameworks/web.py:141 umake/frameworks/web.py:226 msgid "Visual Studio focused on modern web and cloud" -msgstr "Visual Studio 專注於現代 網路和雲端" +msgstr "Visual Studio 專注於現代網頁和雲端" #: umake/frameworks/web.py:223 msgid "Visual Studio Code" @@ -268,7 +256,7 @@ msgstr "遊戲開發環境" #: umake/frameworks/games.py:48 msgid "Stencyl game developer IDE" -msgstr "Stencyl 遊戲開發者整合式開發環境" +msgstr "Stencyl 遊戲開發者整合式開發環境 (IDE)" #: umake/frameworks/games.py:85 msgid "Stencyl" @@ -276,16 +264,15 @@ msgstr "Stencyl" #: umake/frameworks/games.py:114 msgid "Unity 3D Editor Linux experimental support" -msgstr "Unity 3D 編輯器 Linux 實驗性支持" +msgstr "Unity 3D 編輯器 Linux 實驗性支援" #: umake/frameworks/games.py:154 -#, fuzzy msgid "Unity3D Editor" -msgstr "Dart 編輯器" +msgstr "Unity3D 編輯器" #: umake/frameworks/games.py:164 msgid "Twine tool for creating interactive and nonlinear stories" -msgstr "用於創建互動式和非線性故事的 Twine 工具" +msgstr "用於創作互動式和非線性故事的 Twine 工具" #: umake/frameworks/games.py:200 msgid "Twine" @@ -293,7 +280,7 @@ msgstr "Twine" #: umake/interactions/__init__.py:73 msgid "No suitable answer provided" -msgstr "沒有得到適當的答案" +msgstr "沒有提供合適的答案" #: umake/interactions/__init__.py:75 umake/interactions/__init__.py:83 msgid "Your entry '{}' isn't an acceptable choice. choices are: {}" @@ -359,4 +346,4 @@ msgstr "否" #~ msgstr "Stencyl 遊戲開發環境" msgid "Dart Editor for the dart language" -msgstr "用於 Dart 語言的 Dart 編輯器" +msgstr "用於 Dart 程式語言的 Dart 編輯器"