# -*- coding: utf-8 -*- # Copyright (c) 2022 Salvador E. Tropea # Copyright (c) 2022 Instituto Nacional de TecnologĂ­a Industrial # License: GPL-3.0 # Project: KiBot (formerly KiPlot) """ Dependencies: - from: Git role: Find commit hash and/or date - from: Bash role: Run external commands to create replacement text """ import json import os import re from subprocess import run, PIPE from .error import KiPlotConfigurationError from .misc import FAILED_EXECUTE, W_EMPTREP from .optionable import Optionable from .pre_base import BasePreFlight from .gs import GS from .macros import macros, document, pre_class # noqa: F401 from . import log logger = log.get_logger() re_git = re.compile(r'([^a-zA-Z_]|^)(git) ') class KiCadVariable(Optionable): """ KiCad variable definition """ def __init__(self): super().__init__() self._unknown_is_error = True with document: self.name = '' """ Name of the variable. The `version` variable will be expanded using `${version}` """ self.variable = None """ {name} """ self.text = '' """ Text to insert instead of the variable """ self.command = '' """ Command to execute to get the text, will be used only if `text` is empty. This command will be executed using the Bash shell. Be careful about spaces in file names (i.e. use "$KIBOT_PCB_NAME"). The `KIBOT_PCB_NAME` environment variable is the PCB file and the `KIBOT_SCH_NAME` environment variable is the schematic file """ self.before = '' """ Text to add before the output of `command` """ self.after = '' """ Text to add after the output of `command` """ self.expand_kibot_patterns = True """ Expand %X patterns. The context is `schematic` """ def config(self, parent): super().config(parent) if not self.name: raise KiPlotConfigurationError("Missing variable name ({})".format(str(self._tree))) class Set_Text_VariablesOptions(Optionable): """ A list of KiCad variables """ def __init__(self): super().__init__() with document: self.variables = KiCadVariable """ [dict|list(dict)] Variables """ def config(self, parent): super().config(parent) if isinstance(self.variables, type): self.variables = [] elif isinstance(self.variables, KiCadVariable): self.variables = [self.variables] @pre_class class Set_Text_Variables(BasePreFlight): # noqa: F821 """ [dict|list(dict)] Defines KiCad 6+ variables. They are expanded using `${VARIABLE}`, and stored in the project file. This preflight replaces `pcb_replace` and `sch_replace` when using KiCad 6. The KiCad project file is modified. Warning: don't use `-s all` or this preflight will be skipped """ def __init__(self, name, value): f = Set_Text_VariablesOptions() f.set_tree({'variables': value}) f.config(self) super().__init__(name, f.variables) @classmethod def get_doc(cls): return cls.__doc__, KiCadVariable @classmethod def get_example(cls): """ Returns a YAML value for the example config """ return ("\n - name: 'git_hash'" "\n command: 'git log -1 --format=\"%h\" \"$KIBOT_PCB_NAME\"'" "\n before: 'Git hash: <'" "\n after: '>'") def apply(self): o = self._value if len(o) == 0: return if GS.ki5: raise KiPlotConfigurationError("The `set_text_variables` preflight is for KiCad 6 or newer") pro_name = GS.pro_file if not pro_name or not os.path.isfile(pro_name): raise KiPlotConfigurationError("Trying to define KiCad 6 variables but the project is missing ({})". format(pro_name)) # Get the current definitions with open(pro_name, 'rt') as f: pro_text = f.read() data = json.loads(pro_text) text_variables = data.get('text_variables', {}) GS.pro_variables = text_variables logger.debug("- Current variables: {}".format(text_variables)) # Define the requested variables if GS.pcb_file: os.environ['KIBOT_PCB_NAME'] = GS.pcb_file if GS.sch_file: os.environ['KIBOT_SCH_NAME'] = GS.sch_file bash_command = None for r in o: text = r.text if not text and r.command: command = r.command if re_git.search(command): git_command = self.ensure_tool('git') command = re_git.sub(r'\1'+git_command.replace('\\', r'\\')+' ', command) if not bash_command: bash_command = self.ensure_tool('Bash') cmd = [bash_command, '-c', command] logger.debug('Executing: '+GS.pasteable_cmd(command)) result = run(cmd, stdout=PIPE, stderr=PIPE, universal_newlines=True) if result.returncode: msgs = [f'Failed to execute:\n{r.command}\nreturn code {result.returncode}'] if result.stdout: msgs.append(f'stdout:\n{result.stdout}') if result.stderr: msgs.append(f'stderr:\n{result.stderr}') GS.exit_with_error(msgs, FAILED_EXECUTE) if not result.stdout: logger.warning(W_EMPTREP+"Empty value from `{}`".format(r.command)) text = result.stdout.strip() text = r.before + text + r.after logger.debug(' - ' + r.name + ' -> ' + text) text_variables[r.name] = text logger.debug("- Expanding %X patterns in variables") # Now that we have the variables defined expand the %X patterns (they could use variables) for r in o: if r.expand_kibot_patterns: text = text_variables[r.name] new_text = Optionable.expand_filename_both(self, text, make_safe=False) if text != new_text: logger.debug(' - ' + r.name + ' -> ' + new_text) text_variables[r.name] = new_text logger.debug("- New list of variables: {}".format(text_variables)) # Store the modified project data['text_variables'] = text_variables GS.make_bkp(pro_name) with open(pro_name, 'wt') as f: f.write(json.dumps(data, sort_keys=True, indent=2)) if GS.board: # Force a project and PCB reload GS.reload_project(pro_name) # Check if we need to force a PCB text variables reset if GS.global_invalidate_pcb_text_cache == 'auto': logger.debug('Forcing PCB text variables reset') GS.global_invalidate_pcb_text_cache = 'yes'