diff --git a/api-docs/source/conf.py b/api-docs/source/conf.py index 1c95db7418..c7a4d866b1 100644 --- a/api-docs/source/conf.py +++ b/api-docs/source/conf.py @@ -445,8 +445,10 @@ def generaterst(): # # The short X.Y version. version = u'.'.join(str(binaryninja.core_version()).split('.')[0:2]) -# The longer X.Y.Z-channel version. (We intentionally strip the edition.) -release = str(binaryninja.core_version().split(' ')[0]) +# The longer X.Y.Z version. We keep the [branch] marker test builds carry, but +# intentionally strip the edition (Personal/Ultimate/Headless/free). +_version_parts = str(binaryninja.core_version()).split() +release = ' '.join([_version_parts[0]] + [p for p in _version_parts[1:] if p.startswith('[')]) language = 'en' @@ -501,7 +503,7 @@ def generaterst(): # The name for this set of Sphinx documents. # " v documentation" by default. # -html_title = u'Binary Ninja API Documentation v' + version +html_title = u'Binary Ninja API Documentation v' + release # A shorter title for the navigation bar. Default is the same as html_title. # diff --git a/docs/about/license.md b/docs/about/license.md index d2eb917dc7..5112c1badd 100644 --- a/docs/about/license.md +++ b/docs/about/license.md @@ -6,7 +6,8 @@ Binary Ninja comes in different versions. Depending on the terms under which you - [Non-commercial / Student License (Named)](license/noncommercial-named.md) - [Commercial License (Named)](license/commercial-named.md) - [Commercial License (Computer)](license/commercial-computer.md) -- [Ultimate License](license/ultimate.md) +- [Ultimate License (Named)](license/ultimate-named.md) +- [Ultimate License (Computer)](license/ultimate-computer.md) - [Ultimate Floating License (Enterprise Client)](license/ultimate-floating.md) - [Enterprise Server License](license/enterprise-server.md) diff --git a/docs/about/license/noncommercial-computer.md b/docs/about/license/noncommercial-computer.md index 6be215da67..ac64b1f102 100644 --- a/docs/about/license/noncommercial-computer.md +++ b/docs/about/license/noncommercial-computer.md @@ -1,5 +1,10 @@ # Non-commercial / Student License (COMPUTER) +!!! Warning "Deprecated" + This license is no longer offered and is retained for reference by existing + license holders. See the [current licenses](../license.md) for the options + available today. + BINARY NINJA SOFTWARE LICENSE AGREEMENT (Non-commercial Computer License) diff --git a/docs/about/license/ultimate-computer.md b/docs/about/license/ultimate-computer.md new file mode 100644 index 0000000000..5f3dc16332 --- /dev/null +++ b/docs/about/license/ultimate-computer.md @@ -0,0 +1,35 @@ +# Ultimate License (COMPUTER) + +BINARY NINJA™ ULTIMATE LICENSE + +IMPORTANT! BE SURE TO CAREFULLY READ AND UNDERSTAND ALL OF THE TERMS SET FORTH IN THIS LICENSE AGREEMENT ("LICENSE"). BY CLICKING THE "I ACCEPT" BUTTON, YOU AGREE TO FOLLOW AND BE BOUND BY THE TERMS AND CONDITIONS OF THIS LICENSE. IF YOU DO NOT AGREE TO ALL THE TERMS AND CONDITIONS IN THIS LICENSE, YOU MUST SELECT THE "I DECLINE" BUTTON AND MAY NOT USE THE SOFTWARE. + +This License is entered into by and between you ("you" or "your") and Vector 35 Inc, a Delaware corporation ("us", "we" or "our"). + +We will license Binary Ninja™ Ultimate, a software application (the "Software"), to you under the mutual terms and conditions in this License. By using the Software, you agree to be bound by the terms of this License. If you do not agree to the terms of this License, please do not install or attempt to use the Software. + +1. Non-Exclusive License Grant. Under the terms of this License, the Software is licensed on a non-exclusive basis and is not sold. You receive no title to or ownership of the Software itself. This License grants you the rights to a computer license that allows a copy of the Software to be installed and used by you on a particular single computer you own (the "Designated Computer") which Designated Computer may be used by any user. In other words, you may install the Software only on one computer owned by you but you may allow multiple users to use the Software on such Designated Computer as long as the Designated Computer is the only physical computer running the Software at any time. This License does not permit any concurrent use. If you will use the Software on any computers other than the Designated Computer that the application will be installed on, then you are required to obtain additional licenses for each such computer upon which the Software will be installed. If your needs require concurrent use, please contact us for alternative licensing arrangements. All rights not expressly granted herein reserved by us. + +2. Termination. Your license to the Software automatically terminates if you fail to comply with the terms of this License. Upon termination of this License, all licenses granted in Section 1 will terminate and you are required to stop using the Software and delete all copies in your possession or control. The following provisions will survive termination of this License: (i) your obligation to pay for services rendered before termination; (ii) Sections 6 through 14; and (iii) any other provision of this License that must survive termination to fulfill its essential purpose. + +3. Modification and Upgrades. We may, from time to time, and in certain cases for a fee, replace, modify, or upgrade the Software. The license fee includes one year of free upgrades. When accepted by you, any such replacement or modified Software code or upgrade to the Software will be considered part of the Software and subject to the terms of this License (unless this License is superseded by a further License accompanying such replacement or modified version of or upgrade to the Software). + +4. Restrictions. Subject to applicable copyright, trade secret, and other laws, you are permitted under this License to reverse engineer or de-compile the Software but you may not alter, duplicate, modify, rent, lease, loan, sublicense, create derivative works from or provide others with the Software in whole or part, or transmit or communicate any of the Software over a network in order to share it with others. These restrictions include prohibitions on use of the Software for service bureau or time-sharing purposes or in any other way allow third parties to exploit the Software. Time-sharing means sharing the Software with customers or other third parties and permitting their use of the Software. Service bureau involves your use of the Software on behalf of third parties, instead of your own use. It is your responsibility to determine if your use of the Software is in compliance with applicable laws. + +5. Export Restrictions. You must use the Software in accordance with export laws and this means that you may not export, ship, transmit or re-export the Software, in whole or in part, in violation of any applicable law or regulation including but not limited to applicable export administration regulations issued by the U.S. Department of Commerce. + +6. Disclaimer of Warranties. The Software is provided "as is" which means that we are providing no warranty of any kind. WE MAKE NO WARRANTIES, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY IMPLIED WARRANTIES OF MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. We do not warrant that the Software will perform without error or that it will run without interruption. + +7. Limitation of Liability. IN NO EVENT WILL OUR LIABILITY ARISING OUT OF OR RELATED TO THIS LICENSE EXCEED THE AGGREGATE OF FEES PAYABLE TO US UNDER THIS LICENSE (INCLUDING FEES BOTH PAID AND DUE) AT THE TIME OF THE EVENT GIVING RISE TO THE LIABILITY. IN NO EVENT WILL WE BE LIABLE FOR ANY CONSEQUENTIAL, INDIRECT, SPECIAL, INCIDENTAL, OR PUNITIVE DAMAGES. THE LIABILITIES LIMITED BY THIS SECTION 8 APPLY: (A) TO LIABILITY FOR NEGLIGENCE; (B) REGARDLESS OF THE FORM OF ACTION, WHETHER IN CONTRACT, TORT, STRICT PRODUCT LIABILITY, OR OTHERWISE; (C) EVEN IF WE ARE ADVISED IN ADVANCE OF THE POSSIBILITY OF THE DAMAGES IN QUESTION AND EVEN IF SUCH DAMAGES WERE FORESEEABLE; AND (D) EVEN IF YOUR REMEDIES FAIL OF THEIR ESSENTIAL PURPOSE. If applicable law limits the application of the provisions of this Section 8, our liability will be limited to the maximum extent permissible. + +8. Severability. To the extent permitted by law, we waive and you waive any provision of law that would render any clause of this License invalid or otherwise unenforceable in any respect. In the event that a provision of this License is held to be invalid or otherwise unenforceable, such provision will be interpreted to fulfill its intended purpose to the maximum extent permitted by applicable law, and the remaining provisions of this License will continue in full force and effect. + +9. Independent Contractors. We are not your agent and you are not our agent and so neither party may bind the other in any way. The parties are independent contractors and will represent themselves in all regards as independent contractors. + +10. No Waiver. Neither party will be deemed to have waived any of its rights under this License by lapse of time or by any statement or representation other than in an explicit written waiver. No waiver of a breach of this License will constitute a waiver of any prior or subsequent breach of this License. + +11. Force Majeure. To the extent caused by force majeure, no delay, failure, or default will constitute a breach of this License. + +12. Assignment & Successors. Neither party may assign this License or any of its rights or obligations hereunder without the other's express written consent, except that either party may assign this License to the surviving party in a merger of that party into another entity. Except to the extent forbidden in the previous sentence, this License will be binding upon and inure to the benefit of the respective successors and assigns of the parties. + +13. Choice of Law & Jurisdiction. This License will be governed solely by the internal laws of the State of Florida, without reference to such State's principles of conflicts of law. The parties consent to the personal and exclusive jurisdiction of the federal and state courts in or for Brevard County, Florida. diff --git a/docs/about/license/ultimate.md b/docs/about/license/ultimate-named.md similarity index 99% rename from docs/about/license/ultimate.md rename to docs/about/license/ultimate-named.md index 62c0b2cf4e..9c56509070 100644 --- a/docs/about/license/ultimate.md +++ b/docs/about/license/ultimate-named.md @@ -1,4 +1,4 @@ -# Ultimate License +# Ultimate License (NAMED) BINARY NINJA™ ULTIMATE LICENSE diff --git a/docs/about/open-source.md b/docs/about/open-source.md index f2624c2301..e72195dd63 100644 --- a/docs/about/open-source.md +++ b/docs/about/open-source.md @@ -190,7 +190,7 @@ Please note that we offer no support for running Binary Ninja with modified Qt l [Noto Color Emoji]: https://github.com/googlefonts/noto-emoji [sphinx license]: https://github.com/sphinx-doc/sphinx/blob/master/LICENSE.rst [zensical]: https://zensical.org/ -[zensical license]: https://github.com/zensical/zensical/blob/main/LICENSE +[zensical license]: https://github.com/zensical/zensical/blob/master/LICENSE.md [sphinx]: https://www.sphinx-doc.org/en/master/ [sqlite license]: https://www.sqlite.org/copyright.html [sqlite]: https://www.sqlite.org/index.html diff --git a/docs/dev/documentation.md b/docs/dev/documentation.md index c4a3ed0ab3..43cbd5b162 100644 --- a/docs/dev/documentation.md +++ b/docs/dev/documentation.md @@ -32,6 +32,16 @@ echo C++ API documentation available in html/ `scripts/zensical_build.py` runs `zensical build` and then writes the redirect stubs described by `[project.plugins.redirects.redirect_maps]` in `zensical.toml`. +## Validating + +Every build runs `zensical build --strict`, which fails on links to pages or anchors that do not exist. That covers internal references only. External URLs are checked separately by `scripts/check_links.py`, which requests every external URL in `docs/` and reports the file and line of any that fail: + +```bash +poetry run python scripts/check_links.py +``` + +It is slow and depends on the network, so run it out of band rather than as part of a build. Sites that block automated requests are reported separately from broken links and do not affect the exit code unless `--strict` is passed. + ## Changing Changing documentation for the API itself is fairly straightforward. Use [doxygen style comment blocks](https://www.doxygen.nl/manual/docblocks.html) in C++ and C, and [restructured text blocks](https://sphinx-tutorial.readthedocs.io/step-1/) for python for the source. The user documentation is located in the `docs/` folder and the API documentation is generated from the config in the `api-docs` folder. @@ -44,4 +54,3 @@ Changing documentation for the API itself is fairly straightforward. Use [doxyge [zensical]: https://zensical.org/ [breathe]: https://github.com/michaeljones/breathe [sphinx]: https://www.sphinx-doc.org/en/master/ -[doxygen]: https://www.doxygen.nl diff --git a/docs/guide/efiresolver.md b/docs/guide/efiresolver.md index 926b94a88f..6d3940cf7b 100644 --- a/docs/guide/efiresolver.md +++ b/docs/guide/efiresolver.md @@ -39,7 +39,7 @@ steps: !!! Important "GUID Database" An excellent source of proprietary EFI GUIDs is Binarly's - [GUID DB](https://github.com/binarly-io/guiddb/blob/main/guids.json). This file is in the expected format for + [GUID DB](https://github.com/REhints/guiddb/blob/main/guids.json). This file is in the expected format for EFI Resolver's `efi-guids.json`, and can be copied directly to your user folder as a starting point. 2. Define a GUID in the following format: diff --git a/docs/guide/index.md b/docs/guide/index.md index 54755821a1..f761f7867d 100644 --- a/docs/guide/index.md +++ b/docs/guide/index.md @@ -1238,12 +1238,12 @@ The interactive Python prompt also has several built-in "magic" functions and va - `current_variable`: the current selected [`Variable`](https://api.binary.ninja/binaryninja.variable-module.html?highlight=variable#binaryninja.variable.Variable) in a function (Not to be confused with `current_data_var`) - `current_project`: the [`Project`](https://api.binary.ninja/binaryninja.project-module.html#binaryninja.project.Project) the current view belongs to (`None` if the file is not in a project) - `current_thread`: the [`code.InteractiveConsole`](https://docs.python.org/3/library/code.html#code.InteractiveConsole) backing the scripting console -- `current_ui_context`: the current [`UIContext`](https://api.binary.ninja/cpp/class_u_i_context.html) -- `current_ui_view_frame`: the current [`ViewFrame`](https://api.binary.ninja/cpp/class_view_frame.html) -- `current_ui_view`: the current [`View`](https://api.binary.ninja/cpp/class_view.html) -- `current_ui_action_handler`: the current [`UIActionHandler`](https://api.binary.ninja/cpp/class_u_i_action_handler.html) -- `current_ui_view_location`: the current [`ViewLocation`](https://api.binary.ninja/cpp/class_view_location.html) -- `current_ui_action_context`: the current [`UIActionContext`](https://api.binary.ninja/cpp/struct_u_i_action_context.html) +- `current_ui_context`: the current [`UIContext`](https://api.binary.ninja/cpp/group__uicontext.html#class_u_i_context) +- `current_ui_view_frame`: the current [`ViewFrame`](https://api.binary.ninja/cpp/group__viewframe.html#class_view_frame) +- `current_ui_view`: the current [`View`](https://api.binary.ninja/cpp/group__viewframe.html#class_view) +- `current_ui_action_handler`: the current [`UIActionHandler`](https://api.binary.ninja/cpp/group__action.html#class_u_i_action_handler) +- `current_ui_view_location`: the current [`ViewLocation`](https://api.binary.ninja/cpp/group__viewframe.html#class_view_location) +- `current_ui_action_context`: the current [`UIActionContext`](https://api.binary.ninja/cpp/group__action.html#struct_u_i_action_context) - `current_ui_token_state`: the current token state from the UI action context, which backs `current_token` and `current_variable` ### startup.py diff --git a/docs/guide/troubleshooting.md b/docs/guide/troubleshooting.md index c1e72ecbe0..a4805c1a87 100644 --- a/docs/guide/troubleshooting.md +++ b/docs/guide/troubleshooting.md @@ -266,12 +266,9 @@ stdenv.mkDerivation rec { ``` [known issues]: https://github.com/Vector35/binaryninja-api/issues -[libcurl-compat]: https://www.archlinux.org/packages/community/x86_64/libcurl-compat/ -[archrepo]: https://wiki.archlinux.org/index.php/Official_repositories [recover]: https://binary.ninja/recover.html [support]: https://binary.ninja/support.html [purchase]: https://binary.ninja/purchase.html -[unofficial script]: https://gist.github.com/0x1F9F1/64725fbe9acdeafaf39e048e03f4dd9d [slack]: https://slack.binary.ninja [hashes]: https://binary.ninja/js/hashes.js diff --git a/scripts/check_links.py b/scripts/check_links.py new file mode 100644 index 0000000000..d2dc99e3a7 --- /dev/null +++ b/scripts/check_links.py @@ -0,0 +1,340 @@ +#!/usr/bin/env python3 +""" +Check external URLs in the user documentation for reachability. + +Zensical only validates internal references (links between pages and anchor +targets), so external URLs are never verified during a normal docs build. This +script is the out-of-band replacement for the old `mkdocs-htmlproofer-plugin` +that was dropped in the Zensical migration. Run it manually or on a schedule, +not as part of every build. + +Requests are grouped by host: different hosts are checked concurrently, but a +single host is only ever asked for one URL at a time, with a delay in between. +""" + +import argparse +import re +import sys +import time +import urllib.error +import urllib.parse +import urllib.request +from collections import defaultdict +from concurrent.futures import ThreadPoolExecutor +from pathlib import Path + +AGENTS = { + 'chrome': ("Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 " + "(KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36"), + 'safari': ("Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 " + "(KHTML, like Gecko) Version/17.6 Safari/605.1.15"), + 'curl': 'curl/8.7.1', + 'honest': 'binaryninja-docs-link-check (+https://binary.ninja/)', +} + +# Check as a reader does by default, so pages that serve browsers something +# different are judged on what the reader actually gets +DEFAULT_AGENT = 'chrome' + +# Hosts that refuse browser agents but answer simple clients. Measured, not +# guessed: see the probe results in the commit that added this table. Keys match +# a host exactly or any of its parent domains. +SITE_AGENTS = { + 'sourceforge.net': 'curl', + 'sourceforge.io': 'curl', + 'developer.arm.com': 'curl', + 'developers.redhat.com': 'curl', +} + +# Tried in order when the preferred agent is refused, so a host that starts +# blocking browsers does not need a code change to keep being checked +FALLBACK_AGENTS = ('curl', 'honest') + +# Hosts that need more room than the default delay. The Internet Archive throttles +# with a 503 during a bulk pass, which reads as a broken link if we go too fast. +SITE_DELAYS = { + 'web.archive.org': 3.0, + 'archive.org': 3.0, +} + +# Slack refuses anonymous requests to workspace URLs no matter who is asking, so +# these are unverifiable by nature rather than broken. Pass --no-default-ignores +# to check them anyway. +DEFAULT_IGNORES = ( + r'^https?://slack\.binary\.ninja', + r'^https?://[^/]*\.slack\.com', +) + +# Statuses that mean "the server is there but won't talk to us", which is not +# the same as a broken link +UNVERIFIABLE_STATUSES = {401, 403, 405, 429, 999} + +INLINE_LINK = re.compile(r'\]\(\s*]+)') +REFERENCE_DEF = re.compile(r'^\s{0,3}\[[^\]]+\]:\s*\s]+)') +AUTOLINK = re.compile(r'<(https?://[^>\s]+)>') +HTML_ATTR = re.compile(r'(?:href|src)\s*=\s*["\'](https?://[^"\']+)["\']') +INLINE_CODE = re.compile(r'`[^`]*`') +CODE_FENCE = re.compile(r'^\s*(```|~~~)') + + +def extract_urls(path): + """Yield (line_number, url) for every external URL in a markdown file.""" + in_fence = False + + with open(path, 'r', encoding='utf-8') as f: + for line_num, line in enumerate(f, 1): + if CODE_FENCE.match(line): + in_fence = not in_fence + continue + + if in_fence: + continue + + # Backticked URLs are literal text, not links + line = INLINE_CODE.sub('', line) + + for pattern in (INLINE_LINK, REFERENCE_DEF, AUTOLINK, HTML_ATTR): + for match in pattern.finditer(line): + yield line_num, match.group(1).rstrip('.,;') + + +def host_of(url): + return urllib.parse.urlsplit(url).netloc.lower() + + +def lookup(table, host, default): + """Value for a host or any of its parent domains, else the default.""" + labels = host.split('.') + for i in range(len(labels)): + domain = '.'.join(labels[i:]) + if domain in table: + return table[domain] + return default + + +def agents_for(host, default): + """Preferred agent for a host, then the fallbacks, without repeats.""" + preferred = lookup(SITE_AGENTS, host, default) + order = [preferred] + order.extend(name for name in FALLBACK_AGENTS if name != preferred) + return order + + +def request(url, method, timeout, user_agent): + req = urllib.request.Request(url, method=method, headers={ + 'User-Agent': user_agent, + 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', + }) + with urllib.request.urlopen(req, timeout=timeout) as resp: + if method == 'GET': + resp.read(1024) + return resp.status + + +def attempt(url, timeout, user_agent): + """One HEAD-then-GET pass. Returns (status, detail); status None if unreachable.""" + detail = '' + head_status = None + + # HEAD is cheap, but plenty of servers answer it with a status they would + # never give a real GET, so never trust a failing HEAD on its own + for method in ('HEAD', 'GET'): + try: + status = request(url, method, timeout, user_agent) + except urllib.error.HTTPError as e: + status, detail = e.code, e.reason or '' + except Exception as e: # timeouts, DNS, TLS, malformed redirects + detail = f'{type(e).__name__}: {e}' + continue + + if status >= 400 and method == 'HEAD': + head_status = status + continue + + return status, detail if status >= 400 else '' + + # GET never completed, so HEAD's status is the best we have + if head_status is not None: + return head_status, detail + + return None, detail + + +def delay_for(url, args): + return max(args.delay, lookup(SITE_DELAYS, host_of(url), 0)) + + +def check_url(url, args): + """Return (status, detail, agent_label). Status is an int, or None if unreachable.""" + result = (None, '', DEFAULT_AGENT) + delay = delay_for(url, args) + + for label in agents_for(host_of(url), args.agent): + user_agent = AGENTS.get(label, label) + + for retry in range(args.retries + 1): + status, detail = attempt(url, args.timeout, user_agent) + result = (status, detail, label) + + if status == 429 and retry < args.retries: + time.sleep(delay * (2 ** (retry + 1))) + continue + + break + + # Anything else is this host's real answer, whoever is asking + if status not in UNVERIFIABLE_STATUSES: + return result + + time.sleep(delay) + + return result + + +def check_host(urls, args): + """Check one host's URLs in sequence, pausing between requests.""" + results = {} + + for i, url in enumerate(urls): + if i: + time.sleep(delay_for(url, args)) + results[url] = check_url(url, args) + + return results + + +def collect(files, ignores): + locations = defaultdict(list) + skipped = 0 + + for path in files: + for line_num, url in extract_urls(path): + if any(pattern.search(url) for pattern in ignores): + skipped += 1 + continue + locations[url].append(f'{path}:{line_num}') + + return locations, skipped + + +def main(): + parser = argparse.ArgumentParser( + description='Check external URLs in the user documentation for reachability.') + parser.add_argument( + 'paths', nargs='*', + help='Files or directories to check (default: the docs/ directory)') + parser.add_argument( + '-i', '--ignore', action='append', default=[], metavar='REGEX', + help='Skip URLs matching this regex (repeatable)') + parser.add_argument( + '--no-default-ignores', action='store_true', + help=f'Also check the URLs skipped by default ({len(DEFAULT_IGNORES)} patterns)') + parser.add_argument( + '-j', '--jobs', type=int, default=12, + help='Number of hosts to check concurrently (default: 12)') + parser.add_argument( + '-t', '--timeout', type=float, default=15.0, + help='Per-request timeout in seconds (default: 15)') + parser.add_argument( + '-r', '--retries', type=int, default=1, + help='Retries for connection failures and rate limits (default: 1)') + parser.add_argument( + '-d', '--delay', type=float, default=0.5, + help='Seconds between requests to the same host (default: 0.5)') + parser.add_argument( + '-a', '--agent', default=DEFAULT_AGENT, + help=f'Default agent: a name from {sorted(AGENTS)} or a literal ' + f'User-Agent string (default: {DEFAULT_AGENT})') + parser.add_argument( + '-s', '--strict', action='store_true', + help='Also fail on URLs that could not be verified (401/403/429 and friends)') + parser.add_argument( + '-v', '--verbose', action='store_true', + help='List every URL checked, not just the problems') + + args = parser.parse_args() + + if args.paths: + files = [] + for path_str in args.paths: + path = Path(path_str) + if path.is_dir(): + files.extend(sorted(path.rglob('*.md'))) + elif path.is_file(): + files.append(path) + else: + print(f'Warning: {path_str} is not a valid file or directory') + else: + docs_dir = Path(__file__).parent.parent / 'docs' + if not docs_dir.exists(): + print(f'Error: docs directory not found at {docs_dir}') + return 1 + files = sorted(docs_dir.rglob('*.md')) + + patterns = list(args.ignore) + if not args.no_default_ignores: + patterns.extend(DEFAULT_IGNORES) + + # One request per unique URL, but report every place it appears + locations, skipped = collect(files, [re.compile(p) for p in patterns]) + + if not locations: + print('No external URLs found') + return 0 + + by_host = defaultdict(list) + for url in sorted(locations): + by_host[host_of(url)].append(url) + + # Start the hosts with the most URLs first; they set the wall clock + hosts = sorted(by_host.values(), key=len, reverse=True) + + print(f'Checking {len(locations)} unique URL(s) across {len(files)} file(s) ' + f'on {len(by_host)} host(s)...') + + results = {} + with ThreadPoolExecutor(max_workers=args.jobs) as pool: + for batch in pool.map(lambda urls: check_host(urls, args), hosts): + results.update(batch) + + broken = [] + unverified = [] + + for url in sorted(locations): + status, detail, agent = results[url] + note = '' if agent == args.agent else f' (as {agent})' + + if status is None: + broken.append((url, (detail or 'unreachable') + note)) + elif status in UNVERIFIABLE_STATUSES: + unverified.append((url, f'HTTP {status}{note}')) + elif status >= 400: + broken.append((url, f'HTTP {status}{" " + detail if detail else ""}{note}')) + elif args.verbose: + print(f' OK {status} {url}{note}') + + def report(title, entries): + print(f'\n{title}:') + for url, reason in entries: + print(f' {url}') + print(f' {reason}') + for location in locations[url]: + print(f' at {location}') + + if broken: + report(f'{len(broken)} broken URL(s)', broken) + + if unverified: + report(f'{len(unverified)} URL(s) could not be verified', unverified) + + print(f'\n{len(locations)} checked, {len(broken)} broken, ' + f'{len(unverified)} unverified, {skipped} ignored') + + if broken or (args.strict and unverified): + return 1 + + return 0 + + +if __name__ == '__main__': + sys.exit(main()) diff --git a/sphinx_rtd/layout.html b/sphinx_rtd/layout.html index cb7b18266c..1e23910b3e 100644 --- a/sphinx_rtd/layout.html +++ b/sphinx_rtd/layout.html @@ -141,12 +141,11 @@ {%- endif %} {%- if not (logo_url and theme_logo_only) %} - {%- set nav_version = version %} + {%- set nav_version = release %} {%- if READTHEDOCS and current_version %} {%- set nav_version = current_version %} {%- endif %} - {#- The version string can carry the edition (e.g. "5.4 Ultimate"); show only the number #} - Python API {% if theme_display_version and nav_version %} ({{ nav_version.split(' ') | first }}){% endif %} + Python API{% if theme_display_version and nav_version %} ({{ nav_version }}){% endif %} {%- endif %} {%- include "searchbox.html" %} diff --git a/zensical.toml b/zensical.toml index 09b173adfe..91958c76fc 100644 --- a/zensical.toml +++ b/zensical.toml @@ -103,10 +103,11 @@ nav = [ "about/license.md", { "Non-commercial (Named)" = "about/license/noncommercial-named.md" }, { "Commercial (Named)" = "about/license/commercial-named.md" }, - { "Non-commercial (Computer)" = "about/license/noncommercial-computer.md" }, + { "Non-commercial (Computer, Deprecated)" = "about/license/noncommercial-computer.md" }, { "Commercial (Computer)" = "about/license/commercial-computer.md" }, { "Free" = "about/license/free.md" }, - { "Ultimate" = "about/license/ultimate.md" }, + { "Ultimate (Named)" = "about/license/ultimate-named.md" }, + { "Ultimate (Computer)" = "about/license/ultimate-computer.md" }, { "Ultimate Floating" = "about/license/ultimate-floating.md" }, { "Enterprise Server" = "about/license/enterprise-server.md" }, ] }, @@ -163,6 +164,7 @@ separator = '[\s\-,:!=\[\]()"/]+|\.(?!\d)|&[lg]t;' "guide/debugger.md" = "guide/debugger/index.md" "guide/remote-debugging.md" = "guide/debugger/remote-debugging.md" "guide/dbgeng-ttd.md" = "guide/debugger/dbgeng-ttd.md" +"about/license/ultimate.md" = "about/license/ultimate-named.md" [project.markdown_extensions.attr_list] [project.markdown_extensions.md_in_html]