def _check_version(branch): proc = subprocess.Popen( ['git', 'show', '-s', '--format=%s', f'refs/heads/{branch}'], stdout=subprocess.PIPE, stderr=subprocess.PIPE) stdout, _ = proc.communicate() msg = stdout.decode('utf-8').strip() m = re.search(r'\d+(\.\d+)+((a|b|rc)\d+)?(\.post\d+)?(\.dev\d+)?', msg, re.X | re.I) previousv = version.parse(m.group()) if m else None currentv = version.parse(mkdocs.__version__) if not previousv: log.warning( 'Version check skipped: No version specified in previous deployment.' ) elif currentv > previousv: log.info( f'Previous deployment was done with MkDocs version {previousv}; ' f'you are deploying with a newer version ({currentv})') elif currentv < previousv: log.error( f'Deployment terminated: Previous deployment was made with MkDocs version {previousv}; ' f'you are attempting to deploy with an older version ({currentv}). Use --ignore-version ' 'to deploy anyway.') raise Abort('Deployment Aborted!')
def gh_deploy(config, message=None, force=False, ignore_version=False, shell=False): if not _is_cwd_git_repo(): log.error('Cannot deploy - this directory does not appear to be a git ' 'repository') remote_branch = config['remote_branch'] remote_name = config['remote_name'] if not ignore_version: _check_version(remote_branch) if message is None: message = default_message sha = _get_current_sha(os.path.dirname(config.config_file_path)) message = message.format(version=mkdocs.__version__, sha=sha) log.info("Copying '%s' to '%s' branch and pushing to GitHub.", config['site_dir'], config['remote_branch']) try: ghp_import.ghp_import(config['site_dir'], mesg=message, remote=remote_name, branch=remote_branch, push=force, use_shell=shell, nojekyll=True) except ghp_import.GhpError as e: log.error("Failed to deploy to GitHub with error: \n{}".format( e.message)) raise Abort('Deployment Aborted!') cname_file = os.path.join(config['site_dir'], 'CNAME') # Does this repository have a CNAME set for GitHub pages? if os.path.isfile(cname_file): # This GitHub pages repository has a CNAME configured. with (open(cname_file, 'r')) as f: cname_host = f.read().strip() log.info(f'Based on your CNAME file, your documentation should be ' f'available shortly at: http://{cname_host}') log.info('NOTE: Your DNS records must be configured appropriately for ' 'your CNAME URL to work.') return host, path = _get_remote_url(remote_name) if host is None: # This could be a GitHub Enterprise deployment. log.info('Your documentation should be available shortly.') else: username, repo = path.split('/', 1) if repo.endswith('.git'): repo = repo[:-len('.git')] url = f'https://{username}.github.io/{repo}/' log.info(f"Your documentation should shortly be available at: {url}")
def _is_cwd_git_repo(): try: proc = subprocess.Popen(['git', 'rev-parse', '--is-inside-work-tree'], stdout=subprocess.PIPE, stderr=subprocess.PIPE) except FileNotFoundError: log.error("Could not find git - is it installed and on your path?") raise Abort('Deployment Aborted!') proc.communicate() return proc.wait() == 0
def serve(config_file=None, dev_addr=None, strict=None, theme=None, theme_dir=None, livereload='livereload', watch_theme=False, **kwargs): """ Start the MkDocs development server By default it will serve the documentation on http://localhost:8000/ and it will rebuild the documentation and refresh the page automatically whenever a file is edited. """ # Create a temporary build directory, and set some options to serve it # PY2 returns a byte string by default. The Unicode prefix ensures a Unicode # string is returned. And it makes MkDocs temp dirs easier to identify. site_dir = tempfile.mkdtemp(prefix='mkdocs_') def builder(): log.info("Building documentation...") config = load_config(config_file=config_file, dev_addr=dev_addr, strict=strict, theme=theme, theme_dir=theme_dir, site_dir=site_dir, **kwargs) # Override a few config settings after validation config['site_url'] = 'http://{}/'.format(config['dev_addr']) live_server = livereload in ['dirty', 'livereload'] dirty = livereload == 'dirty' build(config, live_server=live_server, dirty=dirty) return config try: # Perform the initial build config = builder() host, port = config['dev_addr'] if livereload in ['livereload', 'dirty']: _livereload(host, port, config, builder, site_dir, watch_theme) else: _static_server(host, port, site_dir) except OSError as e: # pragma: no cover # Avoid ugly, unhelpful traceback raise Abort(str(e)) finally: shutil.rmtree(site_dir)
def build(config, live_server=False, dirty=False): """ Perform a full site build. """ logger = logging.getLogger('mkdocs') # Add CountHandler for strict mode warning_counter = utils.CountHandler() warning_counter.setLevel(logging.WARNING) if config['strict']: logging.getLogger('mkdocs').addHandler(warning_counter) try: from time import time start = time() # Run `config` plugin events. config = config['plugins'].run_event('config', config) # Run `pre_build` plugin events. config['plugins'].run_event('pre_build', config=config) if not dirty: log.info("Cleaning site directory") utils.clean_directory(config['site_dir']) else: # pragma: no cover # Warn user about problems that may occur with --dirty option log.warning( "A 'dirty' build is being performed, this will likely lead to inaccurate navigation and other" " links within your site. This option is designed for site development purposes only." ) if not live_server: # pragma: no cover log.info( f"Building documentation to directory: {config['site_dir']}") if dirty and site_directory_contains_stale_files( config['site_dir']): log.info( "The directory contains stale files. Use --clean to remove them." ) # First gather all data from all files/pages to ensure all data is consistent across all pages. files = get_files(config) env = config['theme'].get_env() files.add_files_from_theme(env, config) # Run `files` plugin events. files = config['plugins'].run_event('files', files, config=config) nav = get_navigation(files, config) # Run `nav` plugin events. nav = config['plugins'].run_event('nav', nav, config=config, files=files) log.debug("Reading markdown pages.") for file in files.documentation_pages(): log.debug(f"Reading: {file.src_path}") _populate_page(file.page, config, files, dirty) # Run `env` plugin events. env = config['plugins'].run_event('env', env, config=config, files=files) # Start writing files to site_dir now that all data is gathered. Note that order matters. Files # with lower precedence get written first so that files with higher precedence can overwrite them. log.debug("Copying static assets.") files.copy_static_files(dirty=dirty) for template in config['theme'].static_templates: _build_theme_template(template, env, files, config, nav) for template in config['extra_templates']: _build_extra_template(template, files, config, nav) log.debug("Building markdown pages.") doc_files = files.documentation_pages() for file in doc_files: _build_page(file.page, config, doc_files, nav, env, dirty) # Run `post_build` plugin events. config['plugins'].run_event('post_build', config=config) counts = warning_counter.get_counts() if counts: msg = ', '.join([f'{v} {k.lower()}s' for k, v in counts]) raise Abort(f'\nAborted with {msg} in strict mode!') log.info('Documentation built in %.2f seconds', time() - start) except Exception as e: # Run `build_error` plugin events. config['plugins'].run_event('build_error', error=e) if isinstance(e, BuildError): log.error(str(e)) raise Abort('\nAborted with a BuildError!') raise finally: logger.removeHandler(warning_counter)
def serve(config_file=None, dev_addr=None, strict=None, theme=None, theme_dir=None, livereload='livereload', watch_theme=False, watch=[], **kwargs): """ Start the MkDocs development server By default it will serve the documentation on http://localhost:8000/ and it will rebuild the documentation and refresh the page automatically whenever a file is edited. """ # Create a temporary build directory, and set some options to serve it # PY2 returns a byte string by default. The Unicode prefix ensures a Unicode # string is returned. And it makes MkDocs temp dirs easier to identify. site_dir = tempfile.mkdtemp(prefix='mkdocs_') def mount_path(config): return urlsplit(config['site_url'] or '/').path def builder(): log.info("Building documentation...") config = load_config( config_file=config_file, dev_addr=dev_addr, strict=strict, theme=theme, theme_dir=theme_dir, site_dir=site_dir, **kwargs ) # combine CLI watch arguments with config file values if config["watch"] is None: config["watch"] = watch else: config["watch"].extend(watch) # Override a few config settings after validation config['site_url'] = 'http://{}{}'.format(config['dev_addr'], mount_path(config)) live_server = livereload in ['dirty', 'livereload'] dirty = livereload == 'dirty' build(config, live_server=live_server, dirty=dirty) return config try: # Perform the initial build config = builder() host, port = config['dev_addr'] server = LiveReloadServer(builder=builder, host=host, port=port, root=site_dir, mount_path=mount_path(config)) def error_handler(code): if code in (404, 500): error_page = join(site_dir, f'{code}.html') if isfile(error_page): with open(error_page, 'rb') as f: return f.read() server.error_handler = error_handler if livereload in ['livereload', 'dirty']: # Watch the documentation files, the config file and the theme files. server.watch(config['docs_dir']) server.watch(config['config_file_path']) if watch_theme: for d in config['theme'].dirs: server.watch(d) # Run `serve` plugin events. server = config['plugins'].run_event('serve', server, config=config, builder=builder) for item in config['watch']: server.watch(item) try: server.serve() except KeyboardInterrupt: log.info("Shutting down...") finally: server.shutdown() except jinja2.exceptions.TemplateError: # This is a subclass of OSError, but shouldn't be suppressed. raise except OSError as e: # pragma: no cover # Avoid ugly, unhelpful traceback raise Abort(f'{type(e).__name__}: {e}') finally: if isdir(site_dir): shutil.rmtree(site_dir)