diff --git a/docs/about/release-notes.md b/docs/about/release-notes.md index 7decc207..c8e491f8 100644 --- a/docs/about/release-notes.md +++ b/docs/about/release-notes.md @@ -92,6 +92,26 @@ to support such customization. [blocks]: ../user-guide/styling-your-docs/#overriding-template-blocks +#### Support for dirty builds. (#990) + +For large sites the build time required to create the pages can become problematic, +thus a "dirty" build mode was created. This mode simply compares the modified time +of the generated html and source markdown. If the markdown has changed since the +html then the page is re-constructed. Otherwise, the page remains as is. This mode +may be invoked in both the `mkdocs serve` and `mkdocs build` commands: + +```text +mkdocs serve --dirtyreload +``` + +```text +mkdocs build --dirty +``` + +It is important to note that this method for building the pages is for development +of content only, since the navigation and other links do not get updated on other +pages. + ### Other Changes and Additions to Version 0.16.0 * Bugfix: Support `gh-deploy` command on Windows with Python 3 (#722) diff --git a/docs/user-guide/deploying-your-docs.md b/docs/user-guide/deploying-your-docs.md index 3d7e17db..9fc97368 100644 --- a/docs/user-guide/deploying-your-docs.md +++ b/docs/user-guide/deploying-your-docs.md @@ -12,7 +12,7 @@ the primary working branch (usually `master`) of the git repository where you maintain the source documentation for your project, run the following command: ```sh -mkdocs gh-deploy --clean +mkdocs gh-deploy ``` That's it! Behind the scenes, MkDocs will build your docs and use the [ghp-import] @@ -66,7 +66,7 @@ host documentation for your project. Run the following commands from your project's root directory to upload your documentation: ```sh -mkdocs build --clean +mkdocs build python setup.py upload_docs --upload-dir=site ``` @@ -113,7 +113,7 @@ For example, a typical set of commands from the command line might look something like this: ```sh -mkdocs build --clean +mkdocs build scp -r ./site user@host:/path/to/server/root ``` diff --git a/mkdocs/__main__.py b/mkdocs/__main__.py index d790371e..9417c3ad 100644 --- a/mkdocs/__main__.py +++ b/mkdocs/__main__.py @@ -77,7 +77,9 @@ theme_dir_help = "The theme directory to use when building your documentation." theme_help = "The theme to use when building your documentation." theme_choices = utils.get_theme_names() site_dir_help = "The directory to output the result of the documentation build." -reload_help = "Enable and disable the live reloading in the development server." +reload_help = "Enable the live reloading in the development server (this is the default)" +no_reload_help = "Disable the live reloading in the development server." +dirty_reload_help = "Enable the live reloading in the development server, but only re-build files that have changed" commit_message_help = ("A commit message to use when commiting to the " "Github Pages remote branch") remote_branch_help = ("The remote branch to commit to for Github Pages. This " @@ -101,7 +103,9 @@ def cli(): @click.option('-s', '--strict', is_flag=True, help=strict_help) @click.option('-t', '--theme', type=click.Choice(theme_choices), help=theme_help) @click.option('-e', '--theme-dir', type=click.Path(), help=theme_dir_help) -@click.option('--livereload/--no-livereload', default=True, help=reload_help) +@click.option('--livereload', 'livereload', flag_value='livereload', help=reload_help) +@click.option('--no-livereload', 'livereload', flag_value='no-livereload', help=no_reload_help) +@click.option('-d', '--dirtyreload', 'livereload', flag_value='dirty', help=dirty_reload_help) @common_options def serve_command(dev_addr, config_file, strict, theme, theme_dir, livereload): """Run the builtin development server""" @@ -115,7 +119,7 @@ def serve_command(dev_addr, config_file, strict, theme, theme_dir, livereload): strict=strict, theme=theme, theme_dir=theme_dir, - livereload=livereload, + livereload=livereload ) except (exceptions.ConfigurationError, socket.error) as e: # Avoid ugly, unhelpful traceback @@ -123,7 +127,7 @@ def serve_command(dev_addr, config_file, strict, theme, theme_dir, livereload): @cli.command(name="build") -@click.option('-c', '--clean', is_flag=True, help=clean_help) +@click.option('-c', '--clean/--dirty', is_flag=True, help=clean_help) @click.option('-f', '--config-file', type=click.File('rb'), help=config_help) @click.option('-s', '--strict', is_flag=True, help=strict_help) @click.option('-t', '--theme', type=click.Choice(theme_choices), help=theme_help) @@ -139,7 +143,7 @@ def build_command(clean, config_file, strict, theme, theme_dir, site_dir): theme=theme, theme_dir=theme_dir, site_dir=site_dir - ), clean_site_dir=clean) + ), dirty=not clean) except exceptions.ConfigurationError as e: # Avoid ugly, unhelpful traceback raise SystemExit('\n' + str(e)) @@ -169,7 +173,7 @@ def json_command(clean, config_file, strict, site_dir): config_file=config_file, strict=strict, site_dir=site_dir - ), dump_json=True, clean_site_dir=clean) + ), dump_json=True, dirty=not clean) except exceptions.ConfigurationError as e: # Avoid ugly, unhelpful traceback raise SystemExit('\n' + str(e)) @@ -190,7 +194,7 @@ def gh_deploy_command(config_file, clean, message, remote_branch, remote_name): remote_branch=remote_branch, remote_name=remote_name ) - build.build(config, clean_site_dir=clean) + build.build(config, dirty=not clean) gh_deploy.gh_deploy(config, message=message) except exceptions.ConfigurationError as e: # Avoid ugly, unhelpful traceback diff --git a/mkdocs/commands/build.py b/mkdocs/commands/build.py index 83e1a1c6..71f5e551 100644 --- a/mkdocs/commands/build.py +++ b/mkdocs/commands/build.py @@ -32,6 +32,15 @@ log = logging.getLogger(__name__) log.addFilter(DuplicateFilter()) +def get_complete_paths(config, page): + """ + Return the complete input/output paths for the supplied page. + """ + input_path = os.path.join(config['docs_dir'], page.input_path) + output_path = os.path.join(config['site_dir'], page.output_path) + return input_path, output_path + + def convert_markdown(markdown_source, config, site_navigation=None): """ Convert the Markdown source file to HTML as per the config and @@ -172,11 +181,12 @@ def build_template(template_name, env, config, site_navigation=None): return True -def _build_page(page, config, site_navigation, env, dump_json): +def _build_page(page, config, site_navigation, env, dump_json, dirty=False): + + # Get the input/output paths + input_path, output_path = get_complete_paths(config, page) # Read the input file - input_path = os.path.join(config['docs_dir'], page.input_path) - try: input_content = io.open(input_path, 'r', encoding='utf-8').read() except IOError: @@ -214,7 +224,6 @@ def _build_page(page, config, site_navigation, env, dump_json): output_content = template.render(context) # Write the output file. - output_path = os.path.join(config['site_dir'], page.output_path) if dump_json: json_context = { 'content': context['content'], @@ -250,7 +259,7 @@ def build_extra_templates(extra_templates, config, site_navigation=None): utils.write_file(output_content.encode('utf-8'), output_path) -def build_pages(config, dump_json=False): +def build_pages(config, dump_json=False, dirty=False): """ Builds all the pages and writes them into the build directory. """ @@ -302,6 +311,13 @@ def build_pages(config, dump_json=False): for page in site_navigation.walk_pages(): try: + + # When --dirty is used, only build the page if the markdown has been modified since the + # previous build of the output. + input_path, output_path = get_complete_paths(config, page) + if dirty and (utils.modified_time(input_path) < utils.modified_time(output_path)): + continue + log.debug("Building page %s", page.input_path) build_result = _build_page(page, config, site_navigation, env, dump_json) @@ -317,20 +333,25 @@ def build_pages(config, dump_json=False): utils.write_file(search_index.encode('utf-8'), json_output_path) -def build(config, live_server=False, dump_json=False, clean_site_dir=False): +def build(config, live_server=False, dump_json=False, dirty=False): """ Perform a full site build. """ - if clean_site_dir: + if not dirty: log.info("Cleaning site directory") utils.clean_directory(config['site_dir']) + else: + # 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: log.info("Building documentation to directory: %s", config['site_dir']) - if not clean_site_dir and site_directory_contains_stale_files(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.") if dump_json: - build_pages(config, dump_json=True) + build_pages(config, dump_json=True, dirty=dirty) return # Reversed as we want to take the media files from the builtin theme @@ -339,14 +360,14 @@ def build(config, live_server=False, dump_json=False, clean_site_dir=False): for theme_dir in reversed(config['theme_dir']): log.debug("Copying static assets from theme: %s", theme_dir) utils.copy_media_files( - theme_dir, config['site_dir'], exclude=['*.py', '*.pyc', '*.html'] + theme_dir, config['site_dir'], exclude=['*.py', '*.pyc', '*.html'], dirty=dirty ) log.debug("Copying static assets from the docs dir.") - utils.copy_media_files(config['docs_dir'], config['site_dir']) + utils.copy_media_files(config['docs_dir'], config['site_dir'], dirty=dirty) log.debug("Building markdown pages.") - build_pages(config) + build_pages(config, dirty=dirty) def site_directory_contains_stale_files(site_directory): diff --git a/mkdocs/commands/serve.py b/mkdocs/commands/serve.py index dc261c21..935982a9 100644 --- a/mkdocs/commands/serve.py +++ b/mkdocs/commands/serve.py @@ -51,7 +51,7 @@ def _static_server(host, port, site_dir): def serve(config_file=None, dev_addr=None, strict=None, theme=None, - theme_dir=None, livereload=True): + theme_dir=None, livereload='livereload'): """ Start the MkDocs development server @@ -59,6 +59,7 @@ def serve(config_file=None, dev_addr=None, strict=None, theme=None, 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 tempdir = tempfile.mkdtemp() @@ -69,10 +70,12 @@ def serve(config_file=None, dev_addr=None, strict=None, theme=None, dev_addr=dev_addr, strict=strict, theme=theme, - theme_dir=theme_dir, + theme_dir=theme_dir ) config['site_dir'] = tempdir - build(config, live_server=True, clean_site_dir=True) + live_server = livereload in ['dirty', 'livereload'] + dirty = livereload == 'dirtyreload' + build(config, live_server=live_server, dirty=dirty) return config # Perform the initial build @@ -81,7 +84,7 @@ def serve(config_file=None, dev_addr=None, strict=None, theme=None, host, port = config['dev_addr'].split(':', 1) try: - if livereload: + if livereload in ['livereload', 'dirtyreload']: _livereload(host, port, config, builder, tempdir) else: _static_server(host, port, tempdir) diff --git a/mkdocs/utils/__init__.py b/mkdocs/utils/__init__.py index bb6c6231..f3134353 100644 --- a/mkdocs/utils/__init__.py +++ b/mkdocs/utils/__init__.py @@ -81,6 +81,17 @@ def yaml_load(source, loader=yaml.Loader): source.close() +def modified_time(file_path): + """ + Return the modified time of the supplied file. If the file does not exists zero is returned. + see build_pages for use. + """ + if os.path.exists(file_path): + return os.path.getmtime(file_path) + else: + return 0.0 + + def reduce_list(data_set): """ Reduce duplicate items in a list and preserve order """ seen = set() @@ -92,6 +103,7 @@ def copy_file(source_path, output_path): """ Copy source_path to output_path, making sure any parent directories exist. """ + output_dir = os.path.dirname(output_path) if not os.path.exists(output_dir): os.makedirs(output_dir) @@ -129,7 +141,7 @@ def clean_directory(directory): os.unlink(path) -def copy_media_files(from_dir, to_dir, exclude=None): +def copy_media_files(from_dir, to_dir, exclude=None, dirty=False): """ Recursively copy all files except markdown and exclude[ed] files into another directory. @@ -155,6 +167,11 @@ def copy_media_files(from_dir, to_dir, exclude=None): if not is_markdown_file(filename): source_path = os.path.join(source_dir, filename) output_path = os.path.join(output_dir, filename) + + # Do not copy when using --dirty if the file has not been modified + if dirty and (modified_time(source_path) < modified_time(output_path)): + continue + copy_file(source_path, output_path)