diff --git a/pros/cli/conductor.py b/pros/cli/conductor.py index 79e098f1..d860e6f8 100644 --- a/pros/cli/conductor.py +++ b/pros/cli/conductor.py @@ -91,8 +91,8 @@ def fetch(query: c.BaseTemplate): help="Force apply the template, disregarding if the template is already installed.") @click.option('--remove-empty-dirs/--no-remove-empty-dirs', 'remove_empty_directories', is_flag=True, default=True, help='Remove empty directories when removing files') -@click.option('--beta', is_flag=True, default=False, show_default=True, - help='Allow applying beta templates') +@click.option('--early-access/--disable-early-access', '--early/--disable-early', '-ea/-dea', 'early_access', '--beta/--disable-beta', default=None, + help='Create a project using the PROS 4 kernel') @project_option() @template_query(required=True) @default_options @@ -117,8 +117,6 @@ def apply(project: c.Project, query: c.BaseTemplate, **kwargs): help="Force apply the template, disregarding if the template is already installed.") @click.option('--remove-empty-dirs/--no-remove-empty-dirs', 'remove_empty_directories', is_flag=True, default=True, help='Remove empty directories when removing files') -@click.option('--beta', is_flag=True, default=False, show_default=True, - help='Allow applying beta templates') @project_option() @template_query(required=True) @default_options @@ -144,8 +142,8 @@ def install(ctx: click.Context, **kwargs): help="Force apply the template, disregarding if the template is already installed.") @click.option('--remove-empty-dirs/--no-remove-empty-dirs', 'remove_empty_directories', is_flag=True, default=True, help='Remove empty directories when removing files') -@click.option('--beta', is_flag=True, default=False, show_default=True, - help='Allow upgrading to beta templates') +@click.option('--early-access/--disable-early-access', '--early/--disable-early', '-ea/-dea', 'early_access', '--beta/--disable-beta', default=None, + help='Create a project using the PROS 4 kernel') @project_option() @template_query(required=False) @default_options @@ -206,8 +204,8 @@ def uninstall_template(project: c.Project, query: c.BaseTemplate, remove_user: b help='Compile the project after creation') @click.option('--build-cache', is_flag=True, default=None, show_default=False, help='Build compile commands cache after creation. Overrides --compile-after if both are specified.') -@click.option('--beta', is_flag=True, default=False, show_default=True, - help='Create a project with a beta template') +@click.option('--early-access/--disable-early-access', '--early/--disable-early', '-ea/-dea', 'early_access', '--beta/--disable-beta', default=None, + help='Create a project using the PROS 4 kernel') @click.pass_context @default_options def new_project(ctx: click.Context, path: str, target: str, version: str, @@ -219,6 +217,7 @@ def new_project(ctx: click.Context, path: str, target: str, version: str, Visit https://pros.cs.purdue.edu/v5/cli/conductor.html to learn more """ analytics.send("new-project") + version_source = version.lower() == 'latest' if version.lower() == 'latest' or not version: version = '>0' if not force_system and c.Project.find_project(path) is not None: @@ -229,7 +228,7 @@ def new_project(ctx: click.Context, path: str, target: str, version: str, _conductor = c.Conductor() if target is None: target = _conductor.default_target - project = _conductor.new_project(path, target=target, version=version, + project = _conductor.new_project(path, target=target, version=version, version_source=version_source, force_user=force_user, force_system=force_system, no_default_libs=no_default_libs, **kwargs) ui.echo('New PROS Project was created:', output_machine=False) @@ -256,13 +255,13 @@ def new_project(ctx: click.Context, path: str, target: str, version: str, help='Force update all remote depots, ignoring automatic update checks') @click.option('--limit', type=int, default=15, help='The maximum number of displayed results for each library') -@click.option('--beta', is_flag=True, default=False, show_default=True, - help='View beta templates in the listing') +@click.option('--early-access/--disable-early-access', '--early/--disable-early', '-ea/-dea', 'early_access', '--beta/--disable-beta', default=None, + help='View a list of early access templates') @template_query(required=False) @click.pass_context @default_options def query_templates(ctx, query: c.BaseTemplate, allow_offline: bool, allow_online: bool, force_refresh: bool, - limit: int, beta: bool): + limit: int, early_access: bool): """ Query local and remote templates based on a spec @@ -272,10 +271,10 @@ def query_templates(ctx, query: c.BaseTemplate, allow_offline: bool, allow_onlin if limit < 0: limit = 15 templates = c.Conductor().resolve_templates(query, allow_offline=allow_offline, allow_online=allow_online, - force_refresh=force_refresh, beta=beta) - if beta: + force_refresh=force_refresh, early_access=early_access) + if early_access: templates += c.Conductor().resolve_templates(query, allow_offline=allow_offline, allow_online=allow_online, - force_refresh=force_refresh, beta=False) + force_refresh=force_refresh, early_access=False) render_templates = {} for template in templates: diff --git a/pros/conductor/conductor.py b/pros/conductor/conductor.py index b8e50416..b3a8e045 100644 --- a/pros/conductor/conductor.py +++ b/pros/conductor/conductor.py @@ -16,12 +16,12 @@ from .templates import BaseTemplate, ExternalTemplate, LocalTemplate, Template MAINLINE_NAME = 'pros-mainline' -MAINLINE_URL = 'https://purduesigbots.github.io/pros-mainline/pros-mainline.json' -BETA_NAME = 'kernel-beta-mainline' -BETA_URL = 'https://raw.githubusercontent.com/purduesigbots/pros-mainline/master/beta/kernel-beta-mainline.json' +MAINLINE_URL = 'https://pros.cs.purdue.edu/v5/_static/releases/pros-mainline.json' +EARLY_ACCESS_NAME = 'kernel-early-access-mainline' +EARLY_ACCESS_URL = 'https://pros.cs.purdue.edu/v5/_static/beta/beta-pros-mainline.json' """ -# TBD? Currently, beta value is stored in config file +# TBD? Currently, EarlyAccess value is stored in config file class ReleaseChannel(Enum): Stable = 'stable' Beta = 'beta' @@ -35,12 +35,13 @@ def __init__(self, file=None): if not file: file = os.path.join(click.get_app_dir('PROS'), 'conductor.pros') self.local_templates: Set[LocalTemplate] = set() - self.beta_local_templates: Set[LocalTemplate] = set() + self.early_access_local_templates: Set[LocalTemplate] = set() self.depots: Dict[str, Depot] = {} self.default_target: str = 'v5' self.default_libraries: Dict[str, List[str]] = None - self.beta_libraries: Dict[str, List[str]] = None - self.is_beta = False + self.early_access_libraries: Dict[str, List[str]] = None + self.use_early_access = False + self.warn_early_access = False super(Conductor, self).__init__(file) needs_saving = False if MAINLINE_NAME not in self.depots or \ @@ -48,11 +49,11 @@ def __init__(self, file=None): self.depots[MAINLINE_NAME].location != MAINLINE_URL: self.depots[MAINLINE_NAME] = HttpDepot(MAINLINE_NAME, MAINLINE_URL) needs_saving = True - # add beta depot as another remote depot - if BETA_NAME not in self.depots or \ - not isinstance(self.depots[BETA_NAME], HttpDepot) or \ - self.depots[BETA_NAME].location != BETA_URL: - self.depots[BETA_NAME] = HttpDepot(BETA_NAME, BETA_URL) + # add early access depot as another remote depot + if EARLY_ACCESS_NAME not in self.depots or \ + not isinstance(self.depots[EARLY_ACCESS_NAME], HttpDepot) or \ + self.depots[EARLY_ACCESS_NAME].location != EARLY_ACCESS_URL: + self.depots[EARLY_ACCESS_NAME] = HttpDepot(EARLY_ACCESS_NAME, EARLY_ACCESS_URL) needs_saving = True if self.default_target is None: self.default_target = 'v5' @@ -63,8 +64,8 @@ def __init__(self, file=None): 'cortex': [] } needs_saving = True - if self.beta_libraries is None or len(self.beta_libraries['v5']) != 2: - self.beta_libraries = { + if self.early_access_libraries is None or len(self.early_access_libraries['v5']) != 2: + self.early_access_libraries = { 'v5': ['liblvgl', 'okapilib'], 'cortex': [] } @@ -75,11 +76,11 @@ def __init__(self, file=None): if 'cortex' not in self.default_libraries: self.default_libraries['cortex'] = [] needs_saving = True - if 'v5' not in self.beta_libraries: - self.beta_libraries['v5'] = [] + if 'v5' not in self.early_access_libraries: + self.early_access_libraries['v5'] = [] needs_saving = True - if 'cortex' not in self.beta_libraries: - self.beta_libraries['cortex'] = [] + if 'cortex' not in self.early_access_libraries: + self.early_access_libraries['cortex'] = [] needs_saving = True if needs_saving: self.save() @@ -106,8 +107,8 @@ def fetch_template(self, depot: Depot, template: BaseTemplate, **kwargs) -> Loca local_template = LocalTemplate(orig=template, location=destination) local_template.metadata['origin'] = depot.name click.echo(f'Adding {local_template.identifier} to registry...', nl=False) - if depot.name == BETA_NAME: # check for beta - self.beta_local_templates.add(local_template) + if depot.name == EARLY_ACCESS_NAME: # check for early access + self.early_access_local_templates.add(local_template) else: self.local_templates.add(local_template) self.save() @@ -117,11 +118,11 @@ def fetch_template(self, depot: Depot, template: BaseTemplate, **kwargs) -> Loca return local_template def purge_template(self, template: LocalTemplate): - if template.metadata['origin'] == BETA_NAME: - if template not in self.beta_local_templates: - logger(__name__).info(f"{template.identifier} was not in the Conductor's local beta templates cache.") + if template.metadata['origin'] == EARLY_ACCESS_NAME: + if template not in self.early_access_local_templates: + logger(__name__).info(f"{template.identifier} was not in the Conductor's local early access templates cache.") else: - self.beta_local_templates.remove(template) + self.early_access_local_templates.remove(template) else: if template not in self.local_templates: logger(__name__).info(f"{template.identifier} was not in the Conductor's local templates cache.") @@ -139,44 +140,40 @@ def resolve_templates(self, identifier: Union[str, BaseTemplate], allow_online: unique: bool = True, **kwargs) -> List[BaseTemplate]: results = list() if not unique else set() kernel_version = kwargs.get('kernel_version', None) - self.is_beta = kwargs.get('beta', False) + if kwargs.get('early_access', None) is not None: + self.use_early_access = kwargs.get('early_access', False) if isinstance(identifier, str): query = BaseTemplate.create_query(name=identifier, **kwargs) else: query = identifier if allow_offline: - if self.is_beta: - offline_results = list(filter(lambda t: t.satisfies(query, kernel_version=kernel_version), self.beta_local_templates)) + if self.use_early_access: + offline_results = list(filter(lambda t: t.satisfies(query, kernel_version=kernel_version), self.early_access_local_templates)) else: offline_results = list(filter(lambda t: t.satisfies(query, kernel_version=kernel_version), self.local_templates)) - if len(offline_results) == 0 and kernel_version and list(filter(lambda t: t.satisfies(query, kernel_version=None), self.local_templates)): - raise dont_send( - InvalidTemplateException(f'{identifier.name} does not support kernel version {kernel_version}')) - if unique: results.update(offline_results) else: results.extend(offline_results) if allow_online: for depot in self.depots.values(): - # beta depot will only be accessed when the --beta flag is true - if depot.name != BETA_NAME or (depot.name == BETA_NAME and self.is_beta): + # EarlyAccess depot will only be accessed when the --early-access flag is true + if depot.name != EARLY_ACCESS_NAME or (depot.name == EARLY_ACCESS_NAME and self.use_early_access): remote_templates = depot.get_remote_templates(force_check=force_refresh, **kwargs) online_results = list(filter(lambda t: t.satisfies(query, kernel_version=kernel_version), remote_templates)) - if len(online_results) == 0 and kernel_version and list(filter(lambda t: t.satisfies(query, kernel_version=None), - remote_templates)): - raise dont_send( - InvalidTemplateException(f'{identifier.name} does not support kernel version {kernel_version}')) - if unique: results.update(online_results) else: results.extend(online_results) logger(__name__).debug('Saving Conductor config after checking for remote updates') self.save() # Save self since there may have been some updates from the depots + + if len(results) == 0 and (kernel_version.split('.')[0] == '3' and not self.use_early_access): + raise dont_send( + InvalidTemplateException(f'{identifier.name} does not support kernel version {kernel_version}')) return list(results) @@ -208,7 +205,7 @@ def resolve_template(self, identifier: Union[str, BaseTemplate], **kwargs) -> Op if len(local_templates) > 1: # This should never happen! Conductor state must be invalid raise Exception(f'Multiple local templates satisfy {query.identifier}!') - return [t for t in templates if isinstance(t, LocalTemplate)][0] + return local_templates[0] # prefer pros-mainline template second mainline_templates = [t for t in templates if t.metadata['origin'] == 'pros-mainline'] @@ -242,8 +239,6 @@ def apply_template(self, project: Project, identifier: Union[str, BaseTemplate], if curr_proj.kernel: if template.version[0] == '4' and curr_proj.kernel[0] == '3': confirm = ui.confirm(f'Warning! Upgrading project to PROS 4 will cause breaking changes. ' - f'For PROS 4 LLEMU/LVGL to function, the library liblvgl is required. ' - f'Run \'pros conductor apply liblvgl --beta\' in the project directory. ' f'Do you still want to upgrade?') if not confirm: raise dont_send( @@ -254,6 +249,19 @@ def apply_template(self, project: Project, identifier: Union[str, BaseTemplate], if not confirm: raise dont_send( InvalidTemplateException(f'Not downgrading')) + elif not self.use_early_access and template.version[0] == '3' and not self.warn_early_access: + confirm = ui.confirm(f'PROS 4 is now in early access. ' + f'Please use the --early-access flag if you would like to use it.\n' + f'Do you want to use PROS 4 instead?') + self.warn_early_access = True + if confirm: # use pros 4 + self.use_early_access = True + kwargs['version'] = '>=0' + self.save() + # Recall the function with early access enabled + return self.apply_template(project, identifier, **kwargs) + + self.save() if not isinstance(template, LocalTemplate): with ui.Notification(): template = self.fetch_template(self.get_depot(template.metadata['origin']), template, **kwargs) @@ -273,11 +281,13 @@ def apply_template(self, project: Project, identifier: Union[str, BaseTemplate], force_user=kwargs.pop('force_user', False), remove_empty_directories=kwargs.pop('remove_empty_directories', False)) ui.finalize('apply', f'Finished applying {template.identifier} to {project.location}') - else: + elif valid_action != TemplateAction.AlreadyInstalled: raise dont_send( InvalidTemplateException(f'Could not install {template.identifier} because it is {valid_action.name},' f' and that is not allowed.', reason=valid_action) ) + else: + ui.finalize('apply', f'{template.identifier} is already installed in {project.location}') @staticmethod def remove_template(project: Project, identifier: Union[str, BaseTemplate], remove_user: bool = True, @@ -291,13 +301,24 @@ def remove_template(project: Project, identifier: Union[str, BaseTemplate], remo remove_empty_directories=remove_empty_directories) def new_project(self, path: str, no_default_libs: bool = False, **kwargs) -> Project: - self.is_beta = kwargs.get('beta', False) + if kwargs.get('early_access', None) is not None: + self.use_early_access = kwargs.get('early_access', False) + if kwargs["version_source"]: # If true, then the user has not specified a version + if not self.use_early_access and self.warn_early_access: + ui.echo(f"PROS 4 is now in early access. " + f"If you would like to use it, use the --early-access flag.") + elif self.use_early_access: + ui.echo(f'Early access is enabled. Using PROS 4.') + elif self.use_early_access: + ui.echo(f'Early access is enabled.') + if Path(path).exists() and Path(path).samefile(os.path.expanduser('~')): raise dont_send(ValueError('Will not create a project in user home directory')) for char in str(Path(path)): if char in ['?', '<', '>', '*', '|', '^', '#', '%', '&', '$', '+', '!', '`', '\'', '=', '@', '\'', '{', '}', '[', ']', '(', ')', '~'] or ord(char) > 127: raise dont_send(ValueError(f'Invalid character found in directory name: \'{char}\'')) + proj = Project(path=path, create=True) if 'target' in kwargs: proj.target = kwargs['target'] @@ -312,25 +333,15 @@ def new_project(self, path: str, no_default_libs: bool = False, **kwargs) -> Pro proj.save() if not no_default_libs: - if self.is_beta: - #libraries = self.beta_libraries if self.is_beta else self.default_libraries - for library in self.beta_libraries[proj.target]: - try: - # remove kernel version so that latest template satisfying query is correctly selected - if 'version' in kwargs: - kwargs.pop('version') - self.apply_template(proj, library, **kwargs) - except Exception as e: - logger(__name__).exception(e) - else: - for library in self.default_libraries[proj.target]: - try: - # remove kernel version so that latest template satisfying query is correctly selected - if 'version' in kwargs: - kwargs.pop('version') - self.apply_template(proj, library, **kwargs) - except Exception as e: - logger(__name__).exception(e) + libraries = self.early_access_libraries if self.use_early_access else self.default_libraries + for library in libraries[proj.target]: + try: + # remove kernel version so that latest template satisfying query is correctly selected + if 'version' in kwargs: + kwargs.pop('version') + self.apply_template(proj, library, **kwargs) + except Exception as e: + logger(__name__).exception(e) return proj def add_depot(self, name: str, url: str): diff --git a/pros/conductor/project/__init__.py b/pros/conductor/project/__init__.py index 76e4d192..b30733b5 100644 --- a/pros/conductor/project/__init__.py +++ b/pros/conductor/project/__init__.py @@ -126,7 +126,7 @@ def apply_template(self, template: LocalTemplate, force_system: bool = False, fo deprecated_user_files = installed_user_files.intersection(self.all_files) - set(template.user_files) if any(deprecated_user_files): if force_user or confirm(f'The following user files have been deprecated: {deprecated_user_files}. ' - f'Do you want to remove them?'): + f'Do you want to update them?'): transaction.extend_rm(deprecated_user_files) else: logger(__name__).warning(f'Deprecated user files may cause weird quirks. See migration guidelines from '