diff --git a/CHANGELOG.md b/CHANGELOG.md index f09db56e..111ea5f3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Option to control the *SVG precision* (units scale) - PCB_Print: - Option to control the *SVG precision* (units scale) +- iBoM: + - Support for the `offset_back_rotation` option ### Changed - Diff: diff --git a/README.md b/README.md index 95512406..c851c5d9 100644 --- a/README.md +++ b/README.md @@ -2154,6 +2154,7 @@ Notes: IBoM option, avoid using in conjunction with KiBot variants/filters. - `no_compression`: [boolean=false] Disable compression of pcb data. - `no_redraw_on_drag`: [boolean=false] Do not redraw pcb on drag by default. + - `offset_back_rotation`: [boolean=false] Offset the back of the pcb by 180 degrees. - `pre_transform`: [string|list(string)='_none'] Name of the filter to transform fields before applying other filters. A short-cut to use for simple cases where a variant is an overkill. - `show_fabrication`: [boolean=false] Show fabrication layer by default. diff --git a/docs/samples/generic_plot.kibot.yaml b/docs/samples/generic_plot.kibot.yaml index 4734ccfe..a1c0adbc 100644 --- a/docs/samples/generic_plot.kibot.yaml +++ b/docs/samples/generic_plot.kibot.yaml @@ -939,6 +939,8 @@ outputs: no_redraw_on_drag: false # [boolean=false] Normalize extra field name case. E.g. 'MPN' and 'mpn' will be considered the same field normalize_field_case: false + # [boolean=false] Offset the back of the pcb by 180 degrees + offset_back_rotation: false # [string='%f-%i%I%v.%x'] Filename for the output, use '' to use the IBoM filename (%i=ibom, %x=html). Affected by global options output: '%f-%i%I%v.%x' # [string|list(string)='_none'] Name of the filter to transform fields before applying other filters. diff --git a/kibot/dep_downloader.py b/kibot/dep_downloader.py index bb14ba24..f75d240e 100644 --- a/kibot/dep_downloader.py +++ b/kibot/dep_downloader.py @@ -175,7 +175,7 @@ def try_download_tar_ball(dep, url, name, name_in_tar=None): name_in_tar = name content = download(url) if content is None: - return None + return None, None # Try to extract the binary dest_file = None try: @@ -186,13 +186,13 @@ def try_download_tar_ball(dep, url, name, name_in_tar=None): dest_file = write_executable(name, tar.extractfile(entry).read()) except Exception as e: logger.debug('- Failed to extract {}'.format(e)) - return None + return None, None # Is this usable? - cmd = check_tool_binary_version(dest_file, dep, no_cache=True) + cmd, ver = check_tool_binary_version(dest_file, dep, no_cache=True) if cmd is None: - return None + return None, None # logger.warning(W_DOWNTOOL+'Using downloaded `{}` tool, please visit {} for details'.format(name, dep.url)) - return cmd + return cmd, ver def untar(data): @@ -276,10 +276,10 @@ def pytool_downloader(dep, system, plat): # Check if we have a github repo as download page logger.debug('- Download URL: '+str(dep.url_down)) if not dep.url_down: - return None + return None, None res = re.match(r'^https://github.com/([^/]+)/([^/]+)/', dep.url_down) if res is None: - return None + return None, None user = res.group(1) prj = res.group(2) logger.debugl(2, '- GitHub repo: {}/{}'.format(user, prj)) @@ -287,27 +287,27 @@ def pytool_downloader(dep, system, plat): # Check if we have pip and wheel pip_command = check_pip() if pip_command is None: - return None + return None, None # Look for the last release data = download(url, progress=False) if data is None: - return None + return None, None try: data = json.loads(data) logger.debugl(4, 'Release information: {}'.format(data)) url = data['tarball_url'] except Exception as e: logger.debug('- Failed to find a download ({})'.format(e)) - return None + return None, None logger.debugl(2, '- Tarball: '+url) # Download and uncompress the tarball dest = untar(download(url)) if dest is None: - return None + return None, None logger.debugl(2, '- Uncompressed tarball to: '+dest) # Try to pip install it if not pip_install(pip_command, dest=dest): - return None + return None, None rmtree(dest) # Check it was successful return check_tool_binary_version(os.path.join(site.USER_BASE, 'bin', dep.command), dep, no_cache=True) @@ -330,13 +330,13 @@ def git_downloader(dep, system, plat): # arm, arm64, mips64el and mipsel are also there, just not implemented if system != 'Linux' or not plat.startswith('x86_'): logger.debug('- No binary for this system') - return None + return None, None # Try to download it arch = 'amd64' if plat == 'x86_64' else 'i386' url = 'https://github.com/EXALAB/git-static/raw/master/output/'+arch+'/bin/git' content = download(url) if content is None: - return None + return None, None dest_bin = write_executable(dep.command+'.real', content.replace(b'/root/output', b'/tmp/kibogit')) # Now create the wrapper git_real = dest_bin @@ -357,31 +357,31 @@ def convert_downloader(dep, system, plat): # Currently only for Linux x86_64 if system != 'Linux' or plat != 'x86_64': logger.debug('- No binary for this system') - return None + return None, None # Get the download page content = download(dep.url_down) if content is None: - return None + return None, None # Look for the URL res = re.search(r'href\s*=\s*"([^"]+)">magick<', content.decode()) if not res: logger.debug('- No `magick` download') - return None + return None, None url = res.group(1) # Get the binary content = download(url) if content is None: - return None + return None, None # Can we run the AppImage? dest_bin = write_executable(dep.command, content) - cmd = check_tool_binary_version(dest_bin, dep, no_cache=True) + cmd, ver = check_tool_binary_version(dest_bin, dep, no_cache=True) if cmd is not None: logger.warning(W_DOWNTOOL+'Using downloaded `{}` tool, please visit {} for details'.format(dep.name, dep.url)) - return cmd + return cmd, ver # Was because we don't have FUSE support if not ('libfuse.so' in last_stderr or 'FUSE' in last_stderr or last_stderr.startswith('fuse')): logger.debug('- Unknown fail reason: `{}`'.format(last_stderr)) - return None + return None, None # Uncompress it unc_dir = os.path.join(home_bin, 'squashfs-root') if os.path.isdir(unc_dir): @@ -392,16 +392,16 @@ def convert_downloader(dep, system, plat): res_run = subprocess.run(cmd, check=True, capture_output=True, cwd=home_bin) except Exception as e: logger.debug('- Failed to execute `{}` ({})'.format(cmd[0], e)) - return None + return None, None if not os.path.isdir(unc_dir): logger.debug('- Failed to uncompress `{}` ({})'.format(cmd[0], res_run.stderr.decode())) - return None + return None, None # Now copy the important stuff # Binaries src_dir, _, bins = next(os.walk(os.path.join(unc_dir, 'usr', 'bin'))) if not len(bins): logger.debug('- No binaries found after extracting {}'.format(dest_bin)) - return None + return None, None for f in bins: dst_file = os.path.join(home_bin, f) if os.path.isfile(dst_file): @@ -411,7 +411,7 @@ def convert_downloader(dep, system, plat): src_dir = os.path.join(unc_dir, 'usr', 'lib') if not os.path.isdir(src_dir): logger.debug('- No libraries found after extracting {}'.format(dest_bin)) - return None + return None, None dst_dir = os.path.join(home_bin, '..', 'lib', 'ImageMagick') if os.path.isdir(dst_dir): rmtree(dst_dir) @@ -422,7 +422,7 @@ def convert_downloader(dep, system, plat): src_dir, dirs, _ = next(os.walk(os.path.join(unc_dir, 'usr', 'etc'))) if len(dirs) != 1: logger.debug('- More than one config dir found {}'.format(dirs)) - return None + return None, None src_dir = os.path.join(src_dir, dirs[0]) dst_dir = os.path.join(home_bin, '..', 'etc') os.makedirs(dst_dir, exist_ok=True) @@ -452,13 +452,13 @@ def gs_downloader(dep, system, plat): # Currently only for Linux x86 if system != 'Linux' or not plat.startswith('x86_'): logger.debug('- No binary for this system') - return None + return None, None # Get the download page url = 'https://api.github.com/repos/ArtifexSoftware/ghostpdl-downloads/releases/latest' r = requests.get(url, allow_redirects=True) if r.status_code != 200: logger.debug('- Failed to download `{}`'.format(dep.url_down)) - return None + return None, None # Look for the valid tarball arch = 'x86_64' if plat == 'x86_64' else 'x86' url = None @@ -472,28 +472,28 @@ def gs_downloader(dep, system, plat): logger.debug('- Failed to find a download ({})'.format(e)) if url is None: logger.debug('- No suitable binary') - return None + return None, None # Try to download it - res = try_download_tar_ball(dep, url, 'gs', 'ghostscript-*/gs*') + res, ver = try_download_tar_ball(dep, url, 'gs', 'ghostscript-*/gs*') if res is not None: short_gs = res long_gs = res[:-2]+'ghostscript' if not os.path.isfile(long_gs): os.symlink(short_gs, long_gs) - return res + return res, ver def rsvg_downloader(dep, system, plat): # Currently only for Linux x86_64 if system != 'Linux' or plat != 'x86_64': logger.debug('- No binary for this system') - return None + return None, None # Get the download page url = 'https://api.github.com/repos/set-soft/rsvg-convert-aws-lambda-binary/releases/latest' r = requests.get(url, allow_redirects=True) if r.status_code != 200: logger.debug('- Failed to download `{}`'.format(dep.url_down)) - return None + return None, None # Look for the valid tarball url = None try: @@ -505,7 +505,7 @@ def rsvg_downloader(dep, system, plat): logger.debug('- Failed to find a download ({})'.format(e)) if url is None: logger.debug('- No suitable binary') - return None + return None, None # Try to download it return try_download_tar_ball(dep, url, 'rsvg-convert') @@ -515,11 +515,11 @@ def rar_downloader(dep, system, plat): r = requests.get(dep.url_down, allow_redirects=True) if r.status_code != 200: logger.debug('- Failed to download `{}`'.format(dep.url_down)) - return None + return None, None # Try to figure out the right package OSs = {'Linux': 'rarlinux', 'Darwin': 'rarmacos'} if system not in OSs: - return None + return None, None name = OSs[system] if plat == 'arm64': name += '-arm' @@ -528,10 +528,10 @@ def rar_downloader(dep, system, plat): elif plat == 'x86_32': name += '-x32' else: - return None + return None, None res = re.search('href="([^"]+{}[^"]+)"'.format(name), r.content.decode()) if not res: - return None + return None, None # Try to download it return try_download_tar_ball(dep, dep.url+res.group(1), 'rar', name_in_tar='rar/rar') @@ -579,7 +579,7 @@ def check_tool_binary_version(full_name, dep, no_cache=False): if dep.no_cmd_line_version: # No way to know the version, assume we can use it logger.debugl(2, "- This tool doesn't have a version option") - return full_name + return full_name, None # Do we need a particular version? needs = (0, 0, 0) for r in dep.roles: @@ -602,7 +602,7 @@ def check_tool_binary_version(full_name, dep, no_cache=False): binary_tools_cache[full_name] = version logger.debugl(2, '- Found version {}'.format(version)) version_check_fail = version is None or version < needs - return None if version_check_fail else full_name + return None if version_check_fail else full_name, version def check_tool_binary_system(dep): @@ -612,7 +612,7 @@ def check_tool_binary_system(dep): else: full_name = which(dep.command) if full_name is None: - return None + return None, None return check_tool_binary_version(full_name, dep) @@ -624,14 +624,14 @@ def check_tool_binary_local(dep): logger.debugl(2, '- Looking for tool `{}` at user level'.format(dep.command)) home = os.environ.get('HOME') or os.environ.get('username') if home is None: - return None + return None, None full_name = os.path.join(home_bin, dep.command) if not os.path.isfile(full_name) or not os.access(full_name, os.X_OK): - return None - cmd = check_tool_binary_version(full_name, dep) + return None, None + cmd, ver = check_tool_binary_version(full_name, dep) if cmd is not None: using_downloaded(dep) - return cmd + return cmd, ver def check_tool_binary_python(dep): @@ -639,13 +639,13 @@ def check_tool_binary_python(dep): logger.debugl(2, '- Looking for tool `{}` at Python user site ({})'.format(dep.command, base)) full_name = os.path.join(base, dep.command) if not os.path.isfile(full_name) or not os.access(full_name, os.X_OK): - return None + return None, None return check_tool_binary_version(full_name, dep) def try_download_tool_binary(dep): if dep.downloader is None or home_bin is None: - return None + return None, None logger.info('- Trying to download {} ({})'.format(dep.name, dep.url_down)) res = None # Determine the platform @@ -663,28 +663,28 @@ def try_download_tool_binary(dep): # res = dep.downloader(dep, system, plat) # return res try: - res = dep.downloader(dep, system, plat) + res, ver = dep.downloader(dep, system, plat) if res: using_downloaded(dep) except Exception as e: logger.error('- Failed to download {}: {}'.format(dep.name, e)) - return res + return res, ver def check_tool_binary(dep): logger.debugl(2, '- Checking binary tool {}'.format(dep.name)) - cmd = check_tool_binary_system(dep) + cmd, ver = check_tool_binary_system(dep) if cmd is not None: - return cmd - cmd = check_tool_binary_python(dep) + return cmd, ver + cmd, ver = check_tool_binary_python(dep) if cmd is not None: - return cmd - cmd = check_tool_binary_local(dep) + return cmd, ver + cmd, ver = check_tool_binary_local(dep) if cmd is not None: - return cmd + return cmd, ver global disable_auto_download if disable_auto_download: - return None + return None, None return try_download_tool_binary(dep) @@ -709,7 +709,7 @@ def check_tool_python_version(mod, dep): version = 'Ok' logger.debugl(2, '- Found version {}'.format(version)) version_check_fail = version != 'Ok' and version < needs - return None if version_check_fail else mod + return None if version_check_fail else mod, version def check_tool_python(dep, reload=False): @@ -723,21 +723,21 @@ def check_tool_python(dep, reload=False): # Not installed, try to download it global disable_auto_download if disable_auto_download or not python_downloader(dep): - return None + return None, None # Check we can use it try: importlib.invalidate_caches() mod = importlib.import_module(dep.module_name) if mod.__file__ is None: logger.error(mod) - return None - res = check_tool_python_version(mod, dep) + return None, None + res, ver = check_tool_python_version(mod, dep) if res is not None and reload: res = importlib.reload(reload) - return res + return res, ver except ModuleNotFoundError: pass - return None + return None, None def do_log_err(msg, fatal): @@ -776,14 +776,14 @@ def get_dep_data(context, dep): return used_deps[context+':'+dep.lower()] -def check_tool_dep(context, dep, fatal=False): +def check_tool_dep_get_ver(context, dep, fatal=False): dep = get_dep_data(context, dep) logger.debug('Starting tool check for {}'.format(dep.name)) if dep.is_python: - cmd = check_tool_python(dep) + cmd, ver = check_tool_python(dep) type = 'python module' else: - cmd = check_tool_binary(dep) + cmd, ver = check_tool_binary(dep) type = 'command' logger.debug('- Returning `{}`'.format(cmd)) if cmd is None: @@ -814,11 +814,17 @@ def check_tool_dep(context, dep, fatal=False): do_log_err(TRY_INSTALL_CHECK, fatal) if fatal: exit(MISSING_TOOL) + return cmd, ver + + +def check_tool_dep(context, dep, fatal=False): + cmd, ver = check_tool_dep_get_ver(context, dep, fatal) return cmd # Avoid circular deps. Optionable can use it. GS.check_tool_dep = check_tool_dep +GS.check_tool_dep_get_ver = check_tool_dep_get_ver class ToolDependencyRole(object): diff --git a/kibot/optionable.py b/kibot/optionable.py index 675f6582..df8030d2 100644 --- a/kibot/optionable.py +++ b/kibot/optionable.py @@ -396,6 +396,10 @@ class BaseOptions(Optionable): """ Looks for a mandatory dependency """ return GS.check_tool_dep(self._parent.type, name, fatal=True) + def ensure_tool_get_ver(self, name): + """ Looks for a mandatory dependency, also returns its version """ + return GS.check_tool_dep_get_ver(self._parent.type, name, fatal=True) + def check_tool(self, name): """ Looks for a dependency """ return GS.check_tool_dep(self._parent.type, name, fatal=False) diff --git a/kibot/out_ibom.py b/kibot/out_ibom.py index 06b92ba9..d4191e81 100644 --- a/kibot/out_ibom.py +++ b/kibot/out_ibom.py @@ -52,6 +52,8 @@ class IBoMOptions(VariantOptions): """ Do not redraw pcb on drag by default """ self.board_rotation = 0 """ *Board rotation in degrees (-180 to 180). Will be rounded to multiple of 5 """ + self.offset_back_rotation = False + """ Offset the back of the pcb by 180 degrees """ self.checkboxes = 'Sourced,Placed' """ Comma separated list of checkbox columns """ self.bom_view = 'left-right' @@ -153,7 +155,7 @@ class IBoMOptions(VariantOptions): def run(self, name): super().run(name) - tool = self.ensure_tool('ibom') + tool, version = self.ensure_tool_get_ver('ibom') logger.debug('Doing Interactive BoM') # Tell ibom we don't want to use the screen os.environ['INTERACTIVE_HTML_BOM_NO_DISPLAY'] = '' @@ -204,6 +206,8 @@ class IBoMOptions(VariantOptions): for k, v in self.get_attrs_gen(): if not v or k in ['output', 'variant', 'dnf_filter', 'pre_transform']: continue + if k == 'offset_back_rotation' and version < (2, 5, 0, 2): + continue cmd.append(BaseOutput.attr2longopt(k)) # noqa: F821 if not isinstance(v, bool): # must be str/(int, float) cmd.append(str(v)) diff --git a/setup.cfg b/setup.cfg index a7f063c8..57d89fb5 100644 --- a/setup.cfg +++ b/setup.cfg @@ -40,6 +40,7 @@ exclude = experiments/kicad/v6/ kibot/mcpyrate/ kibot/PcbDraw/ kibot/PyPDF2/ + kibot/resources submodules/ pp/ output/ diff --git a/tests/test_plot/test_ibom.py b/tests/test_plot/test_ibom.py index ce8984af..44af3e6c 100644 --- a/tests/test_plot/test_ibom.py +++ b/tests/test_plot/test_ibom.py @@ -81,6 +81,7 @@ def test_ibom_all_ops(test_dir): r'"highlight_pin1": true', r'"redraw_on_drag": false', r'"board_rotation": 18.0', # 90/5 + r'"offset_back_rotation": true', r'"checkboxes": "Sourced,Placed,Bogus"', r'"bom_view": "top-bottom"', r'"layer_view": "B"', diff --git a/tests/yaml_samples/ibom_all_ops5.kibot.yaml b/tests/yaml_samples/ibom_all_ops5.kibot.yaml index efcd2c4f..c5dcd5d8 100644 --- a/tests/yaml_samples/ibom_all_ops5.kibot.yaml +++ b/tests/yaml_samples/ibom_all_ops5.kibot.yaml @@ -32,3 +32,4 @@ outputs: variants_whitelist: 'bla,ble,bli' variants_blacklist: 'blo,blu' dnp_field: 'DNP' + offset_back_rotation: true diff --git a/tests/yaml_samples/ibom_all_ops6.kibot.yaml b/tests/yaml_samples/ibom_all_ops6.kibot.yaml index de445e15..911907e4 100644 --- a/tests/yaml_samples/ibom_all_ops6.kibot.yaml +++ b/tests/yaml_samples/ibom_all_ops6.kibot.yaml @@ -32,3 +32,4 @@ outputs: variants_whitelist: 'bla,ble,bli' variants_blacklist: 'blo,blu' dnp_field: 'DNP' + offset_back_rotation: true