This commit is contained in:
2026-06-14 19:09:18 +01:00
parent 14bd1a9271
commit 13fa90a0e9
3958 changed files with 999286 additions and 4 deletions
+15
View File
@@ -0,0 +1,15 @@
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE := fmt_static
LOCAL_MODULE_FILENAME := libfmt
LOCAL_SRC_FILES := ../src/format.cc
LOCAL_C_INCLUDES := $(LOCAL_PATH)
LOCAL_EXPORT_C_INCLUDES := $(LOCAL_PATH)
LOCAL_CFLAGS += -std=c++11 -fexceptions
include $(BUILD_STATIC_LIBRARY)
+1
View File
@@ -0,0 +1 @@
<manifest package="dev.fmt" />
File diff suppressed because it is too large Load Diff
+4
View File
@@ -0,0 +1,4 @@
This directory contains build support files such as
* CMake modules
* Build scripts
+19
View File
@@ -0,0 +1,19 @@
# -*- mode: ruby -*-
# vi: set ft=ruby :
# A vagrant config for testing against gcc-4.8.
Vagrant.configure("2") do |config|
config.vm.box = "bento/ubuntu-22.04-arm64"
config.vm.provider "vmware_desktop" do |vb|
vb.memory = "4096"
end
config.vm.provision "shell", inline: <<-SHELL
apt-get update
apt-get install -y g++ make wget git
wget -q https://github.com/Kitware/CMake/releases/download/v3.26.0/cmake-3.26.0-Linux-x86_64.tar.gz
tar xzf cmake-3.26.0-Linux-x86_64.tar.gz
ln -s `pwd`/cmake-3.26.0-Linux-x86_64/bin/cmake /usr/local/bin
SHELL
end
+1
View File
@@ -0,0 +1 @@
8.1.1
+22
View File
@@ -0,0 +1,22 @@
load("@rules_cc//cc:defs.bzl", "cc_library")
cc_library(
name = "fmt",
srcs = [
#"src/fmt.cc", # No C++ module support, yet in Bazel (https://github.com/bazelbuild/bazel/pull/19940)
"src/format.cc",
"src/os.cc",
],
hdrs = glob([
"include/fmt/*.h",
]),
copts = select({
"@platforms//os:windows": ["-utf-8"],
"//conditions:default": [],
}),
includes = [
"include",
],
strip_include_prefix = "include",
visibility = ["//visibility:public"],
)
+7
View File
@@ -0,0 +1,7 @@
module(
name = "fmt",
compatibility_level = 10,
)
bazel_dep(name = "platforms", version = "0.0.11")
bazel_dep(name = "rules_cc", version = "0.1.1")
+28
View File
@@ -0,0 +1,28 @@
# Bazel support
To get [Bazel](https://bazel.build/) working with {fmt} you can copy the files `BUILD.bazel`,
`MODULE.bazel`, `WORKSPACE.bazel`, and `.bazelversion` from this folder (`support/bazel`) to the root folder of this project.
This way {fmt} gets bazelized and can be used with Bazel (e.g. doing a `bazel build //...` on {fmt}).
## Using {fmt} as a dependency
### Using Bzlmod
The [Bazel Central Registry](https://github.com/bazelbuild/bazel-central-registry/tree/main/modules/fmt) provides support for {fmt}.
For instance, to use {fmt} add to your `MODULE.bazel` file:
```
bazel_dep(name = "fmt", version = "11.1.4")
```
### Live at head
For a live-at-head approach, you can copy the contents of this repository and move the Bazel-related build files to the root folder of this project as described above and make use of `local_path_override`, e.g.:
```
local_path_override(
module_name = "fmt",
path = "../third_party/fmt",
)
```
+2
View File
@@ -0,0 +1,2 @@
# WORKSPACE marker file needed by Bazel
+132
View File
@@ -0,0 +1,132 @@
import java.nio.file.Paths
// General gradle arguments for root project
buildscript {
repositories {
google()
jcenter()
}
dependencies {
//
// https://developer.android.com/studio/releases/gradle-plugin#updating-gradle
//
// Notice that 4.0.0 here is the version of [Android Gradle Plugin]
// According to URL above you will need Gradle 6.1 or higher
//
classpath "com.android.tools.build:gradle:4.1.1"
}
}
repositories {
google()
jcenter()
}
// Project's root where CMakeLists.txt exists: rootDir/support/.cxx -> rootDir
def rootDir = Paths.get(project.buildDir.getParent()).getParent()
println("rootDir: ${rootDir}")
// Output: Shared library (.so) for Android
apply plugin: "com.android.library"
android {
compileSdkVersion 25 // Android 7.0
// Target ABI
// - This option controls target platform of module
// - The platform might be limited by compiler's support
// some can work with Clang(default), but some can work only with GCC...
// if bad, both toolchains might not support it
splits {
abi {
enable true
// Specify platforms for Application
reset()
include "arm64-v8a", "armeabi-v7a", "x86_64"
}
}
ndkVersion "21.3.6528147" // ANDROID_NDK_HOME is deprecated. Be explicit
defaultConfig {
minSdkVersion 21 // Android 5.0+
targetSdkVersion 25 // Follow Compile SDK
versionCode 34 // Follow release count
versionName "7.1.2" // Follow Official version
externalNativeBuild {
cmake {
arguments "-DANDROID_STL=c++_shared" // Specify Android STL
arguments "-DBUILD_SHARED_LIBS=true" // Build shared object
arguments "-DFMT_TEST=false" // Skip test
arguments "-DFMT_DOC=false" // Skip document
cppFlags "-std=c++17"
targets "fmt"
}
}
println(externalNativeBuild.cmake.cppFlags)
println(externalNativeBuild.cmake.arguments)
}
// External Native build
// - Use existing CMakeList.txt
// - Give path to CMake. This gradle file should be
// neighbor of the top level cmake
externalNativeBuild {
cmake {
version "3.10.0+"
path "${rootDir}/CMakeLists.txt"
// buildStagingDirectory "./build" // Custom path for cmake output
}
}
sourceSets{
// Android Manifest for Gradle
main {
manifest.srcFile "AndroidManifest.xml"
}
}
// https://developer.android.com/studio/build/native-dependencies#build_system_configuration
buildFeatures {
prefab true
prefabPublishing true
}
prefab {
fmt {
headers "${rootDir}/include"
}
}
}
assemble.doLast
{
// Instead of `ninja install`, Gradle will deploy the files.
// We are doing this since FMT is dependent to the ANDROID_STL after build
copy {
from "build/intermediates/cmake"
into "${rootDir}/libs"
}
// Copy debug binaries
copy {
from "${rootDir}/libs/debug/obj"
into "${rootDir}/libs/debug"
}
// Copy Release binaries
copy {
from "${rootDir}/libs/release/obj"
into "${rootDir}/libs/release"
}
// Remove empty directory
delete "${rootDir}/libs/debug/obj"
delete "${rootDir}/libs/release/obj"
// Copy AAR files. Notice that the aar is named after the folder of this script.
copy {
from "build/outputs/aar/support-release.aar"
into "${rootDir}/libs"
rename "support-release.aar", "fmt-release.aar"
}
copy {
from "build/outputs/aar/support-debug.aar"
into "${rootDir}/libs"
rename "support-debug.aar", "fmt-debug.aar"
}
}
+43
View File
@@ -0,0 +1,43 @@
#!/usr/bin/env python3
"""Compile source on a range of commits
Usage:
check-commits <start> <source>
"""
import docopt, os, sys, tempfile
from subprocess import check_call, check_output, run
args = docopt.docopt(__doc__)
start = args.get('<start>')
source = args.get('<source>')
cwd = os.getcwd()
with tempfile.TemporaryDirectory() as work_dir:
check_call(['git', 'clone', 'https://github.com/fmtlib/fmt.git'],
cwd=work_dir)
repo_dir = os.path.join(work_dir, 'fmt')
commits = check_output(
['git', 'rev-list', f'{start}..HEAD', '--abbrev-commit',
'--', 'include', 'src'],
text=True, cwd=repo_dir).rstrip().split('\n')
commits.reverse()
print('Time\tCommit')
for commit in commits:
check_call(['git', '-c', 'advice.detachedHead=false', 'checkout', commit],
cwd=repo_dir)
returncode = run(
['c++', '-std=c++11', '-O3', '-DNDEBUG', '-I', 'include',
'src/format.cc', os.path.join(cwd, source)], cwd=repo_dir).returncode
if returncode != 0:
continue
times = []
for i in range(5):
output = check_output([os.path.join(repo_dir, 'a.out')], text=True)
times.append(float(output))
message = check_output(['git', 'log', '-1', '--pretty=format:%s', commit],
cwd=repo_dir, text=True)
print(f'{min(times)}\t{commit} {message[:40]}')
sys.stdout.flush()
+7
View File
@@ -0,0 +1,7 @@
# A CMake script to find SetEnv.cmd.
find_program(WINSDK_SETENV NAMES SetEnv.cmd
PATHS "[HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Microsoft SDKs\\Windows;CurrentInstallFolder]/bin")
if (WINSDK_SETENV AND PRINT_PATH)
execute_process(COMMAND ${CMAKE_COMMAND} -E echo "${WINSDK_SETENV}")
endif ()
+26
View File
@@ -0,0 +1,26 @@
# This module provides function for joining paths
# known from from most languages
#
# Original license:
# SPDX-License-Identifier: (MIT OR CC0-1.0)
# Explicit permission given to distribute this module under
# the terms of the project as described in /LICENSE.rst.
# Copyright 2020 Jan Tojnar
# https://github.com/jtojnar/cmake-snips
#
# Modelled after Pythons os.path.join
# https://docs.python.org/3.7/library/os.path.html#os.path.join
# Windows not supported
function(join_paths joined_path first_path_segment)
set(temp_path "${first_path_segment}")
foreach(current_segment IN LISTS ARGN)
if(NOT ("${current_segment}" STREQUAL ""))
if(IS_ABSOLUTE "${current_segment}")
set(temp_path "${current_segment}")
else()
set(temp_path "${temp_path}/${current_segment}")
endif()
endif()
endforeach()
set(${joined_path} "${temp_path}" PARENT_SCOPE)
endfunction()
+7
View File
@@ -0,0 +1,7 @@
@PACKAGE_INIT@
if (NOT TARGET fmt::fmt)
include(${CMAKE_CURRENT_LIST_DIR}/@targets_export_name@.cmake)
endif ()
check_required_components(fmt)
+11
View File
@@ -0,0 +1,11 @@
prefix=@CMAKE_INSTALL_PREFIX@
exec_prefix=@CMAKE_INSTALL_PREFIX@
libdir=@libdir_for_pc_file@
includedir=@includedir_for_pc_file@
Name: fmt
Description: A modern formatting library
Version: @FMT_VERSION@
Libs: -L${libdir} -l@FMT_LIB_NAME@
Cflags: -I${includedir}
+581
View File
@@ -0,0 +1,581 @@
"""Pythonic command-line interface parser that will make you smile.
* http://docopt.org
* Repository and issue-tracker: https://github.com/docopt/docopt
* Licensed under terms of MIT license (see LICENSE-MIT)
* Copyright (c) 2013 Vladimir Keleshev, vladimir@keleshev.com
"""
import sys
import re
__all__ = ['docopt']
__version__ = '0.6.1'
class DocoptLanguageError(Exception):
"""Error in construction of usage-message by developer."""
class DocoptExit(SystemExit):
"""Exit in case user invoked program with incorrect arguments."""
usage = ''
def __init__(self, message=''):
SystemExit.__init__(self, (message + '\n' + self.usage).strip())
class Pattern(object):
def __eq__(self, other):
return repr(self) == repr(other)
def __hash__(self):
return hash(repr(self))
def fix(self):
self.fix_identities()
self.fix_repeating_arguments()
return self
def fix_identities(self, uniq=None):
"""Make pattern-tree tips point to same object if they are equal."""
if not hasattr(self, 'children'):
return self
uniq = list(set(self.flat())) if uniq is None else uniq
for i, child in enumerate(self.children):
if not hasattr(child, 'children'):
assert child in uniq
self.children[i] = uniq[uniq.index(child)]
else:
child.fix_identities(uniq)
def fix_repeating_arguments(self):
"""Fix elements that should accumulate/increment values."""
either = [list(child.children) for child in transform(self).children]
for case in either:
for e in [child for child in case if case.count(child) > 1]:
if type(e) is Argument or type(e) is Option and e.argcount:
if e.value is None:
e.value = []
elif type(e.value) is not list:
e.value = e.value.split()
if type(e) is Command or type(e) is Option and e.argcount == 0:
e.value = 0
return self
def transform(pattern):
"""Expand pattern into an (almost) equivalent one, but with single Either.
Example: ((-a | -b) (-c | -d)) => (-a -c | -a -d | -b -c | -b -d)
Quirks: [-a] => (-a), (-a...) => (-a -a)
"""
result = []
groups = [[pattern]]
while groups:
children = groups.pop(0)
parents = [Required, Optional, OptionsShortcut, Either, OneOrMore]
if any(t in map(type, children) for t in parents):
child = [c for c in children if type(c) in parents][0]
children.remove(child)
if type(child) is Either:
for c in child.children:
groups.append([c] + children)
elif type(child) is OneOrMore:
groups.append(child.children * 2 + children)
else:
groups.append(child.children + children)
else:
result.append(children)
return Either(*[Required(*e) for e in result])
class LeafPattern(Pattern):
"""Leaf/terminal node of a pattern tree."""
def __init__(self, name, value=None):
self.name, self.value = name, value
def __repr__(self):
return '%s(%r, %r)' % (self.__class__.__name__, self.name, self.value)
def flat(self, *types):
return [self] if not types or type(self) in types else []
def match(self, left, collected=None):
collected = [] if collected is None else collected
pos, match = self.single_match(left)
if match is None:
return False, left, collected
left_ = left[:pos] + left[pos + 1:]
same_name = [a for a in collected if a.name == self.name]
if type(self.value) in (int, list):
if type(self.value) is int:
increment = 1
else:
increment = ([match.value] if type(match.value) is str
else match.value)
if not same_name:
match.value = increment
return True, left_, collected + [match]
same_name[0].value += increment
return True, left_, collected
return True, left_, collected + [match]
class BranchPattern(Pattern):
"""Branch/inner node of a pattern tree."""
def __init__(self, *children):
self.children = list(children)
def __repr__(self):
return '%s(%s)' % (self.__class__.__name__,
', '.join(repr(a) for a in self.children))
def flat(self, *types):
if type(self) in types:
return [self]
return sum([child.flat(*types) for child in self.children], [])
class Argument(LeafPattern):
def single_match(self, left):
for n, pattern in enumerate(left):
if type(pattern) is Argument:
return n, Argument(self.name, pattern.value)
return None, None
@classmethod
def parse(class_, source):
name = re.findall('(<\S*?>)', source)[0]
value = re.findall('\[default: (.*)\]', source, flags=re.I)
return class_(name, value[0] if value else None)
class Command(Argument):
def __init__(self, name, value=False):
self.name, self.value = name, value
def single_match(self, left):
for n, pattern in enumerate(left):
if type(pattern) is Argument:
if pattern.value == self.name:
return n, Command(self.name, True)
else:
break
return None, None
class Option(LeafPattern):
def __init__(self, short=None, long=None, argcount=0, value=False):
assert argcount in (0, 1)
self.short, self.long, self.argcount = short, long, argcount
self.value = None if value is False and argcount else value
@classmethod
def parse(class_, option_description):
short, long, argcount, value = None, None, 0, False
options, _, description = option_description.strip().partition(' ')
options = options.replace(',', ' ').replace('=', ' ')
for s in options.split():
if s.startswith('--'):
long = s
elif s.startswith('-'):
short = s
else:
argcount = 1
if argcount:
matched = re.findall('\[default: (.*)\]', description, flags=re.I)
value = matched[0] if matched else None
return class_(short, long, argcount, value)
def single_match(self, left):
for n, pattern in enumerate(left):
if self.name == pattern.name:
return n, pattern
return None, None
@property
def name(self):
return self.long or self.short
def __repr__(self):
return 'Option(%r, %r, %r, %r)' % (self.short, self.long,
self.argcount, self.value)
class Required(BranchPattern):
def match(self, left, collected=None):
collected = [] if collected is None else collected
l = left
c = collected
for pattern in self.children:
matched, l, c = pattern.match(l, c)
if not matched:
return False, left, collected
return True, l, c
class Optional(BranchPattern):
def match(self, left, collected=None):
collected = [] if collected is None else collected
for pattern in self.children:
m, left, collected = pattern.match(left, collected)
return True, left, collected
class OptionsShortcut(Optional):
"""Marker/placeholder for [options] shortcut."""
class OneOrMore(BranchPattern):
def match(self, left, collected=None):
assert len(self.children) == 1
collected = [] if collected is None else collected
l = left
c = collected
l_ = None
matched = True
times = 0
while matched:
# could it be that something didn't match but changed l or c?
matched, l, c = self.children[0].match(l, c)
times += 1 if matched else 0
if l_ == l:
break
l_ = l
if times >= 1:
return True, l, c
return False, left, collected
class Either(BranchPattern):
def match(self, left, collected=None):
collected = [] if collected is None else collected
outcomes = []
for pattern in self.children:
matched, _, _ = outcome = pattern.match(left, collected)
if matched:
outcomes.append(outcome)
if outcomes:
return min(outcomes, key=lambda outcome: len(outcome[1]))
return False, left, collected
class Tokens(list):
def __init__(self, source, error=DocoptExit):
self += source.split() if hasattr(source, 'split') else source
self.error = error
@staticmethod
def from_pattern(source):
source = re.sub(r'([\[\]\(\)\|]|\.\.\.)', r' \1 ', source)
source = [s for s in re.split('\s+|(\S*<.*?>)', source) if s]
return Tokens(source, error=DocoptLanguageError)
def move(self):
return self.pop(0) if len(self) else None
def current(self):
return self[0] if len(self) else None
def parse_long(tokens, options):
"""long ::= '--' chars [ ( ' ' | '=' ) chars ] ;"""
long, eq, value = tokens.move().partition('=')
assert long.startswith('--')
value = None if eq == value == '' else value
similar = [o for o in options if o.long == long]
if tokens.error is DocoptExit and similar == []: # if no exact match
similar = [o for o in options if o.long and o.long.startswith(long)]
if len(similar) > 1: # might be simply specified ambiguously 2+ times?
raise tokens.error('%s is not a unique prefix: %s?' %
(long, ', '.join(o.long for o in similar)))
elif len(similar) < 1:
argcount = 1 if eq == '=' else 0
o = Option(None, long, argcount)
options.append(o)
if tokens.error is DocoptExit:
o = Option(None, long, argcount, value if argcount else True)
else:
o = Option(similar[0].short, similar[0].long,
similar[0].argcount, similar[0].value)
if o.argcount == 0:
if value is not None:
raise tokens.error('%s must not have an argument' % o.long)
else:
if value is None:
if tokens.current() in [None, '--']:
raise tokens.error('%s requires argument' % o.long)
value = tokens.move()
if tokens.error is DocoptExit:
o.value = value if value is not None else True
return [o]
def parse_shorts(tokens, options):
"""shorts ::= '-' ( chars )* [ [ ' ' ] chars ] ;"""
token = tokens.move()
assert token.startswith('-') and not token.startswith('--')
left = token.lstrip('-')
parsed = []
while left != '':
short, left = '-' + left[0], left[1:]
similar = [o for o in options if o.short == short]
if len(similar) > 1:
raise tokens.error('%s is specified ambiguously %d times' %
(short, len(similar)))
elif len(similar) < 1:
o = Option(short, None, 0)
options.append(o)
if tokens.error is DocoptExit:
o = Option(short, None, 0, True)
else: # why copying is necessary here?
o = Option(short, similar[0].long,
similar[0].argcount, similar[0].value)
value = None
if o.argcount != 0:
if left == '':
if tokens.current() in [None, '--']:
raise tokens.error('%s requires argument' % short)
value = tokens.move()
else:
value = left
left = ''
if tokens.error is DocoptExit:
o.value = value if value is not None else True
parsed.append(o)
return parsed
def parse_pattern(source, options):
tokens = Tokens.from_pattern(source)
result = parse_expr(tokens, options)
if tokens.current() is not None:
raise tokens.error('unexpected ending: %r' % ' '.join(tokens))
return Required(*result)
def parse_expr(tokens, options):
"""expr ::= seq ( '|' seq )* ;"""
seq = parse_seq(tokens, options)
if tokens.current() != '|':
return seq
result = [Required(*seq)] if len(seq) > 1 else seq
while tokens.current() == '|':
tokens.move()
seq = parse_seq(tokens, options)
result += [Required(*seq)] if len(seq) > 1 else seq
return [Either(*result)] if len(result) > 1 else result
def parse_seq(tokens, options):
"""seq ::= ( atom [ '...' ] )* ;"""
result = []
while tokens.current() not in [None, ']', ')', '|']:
atom = parse_atom(tokens, options)
if tokens.current() == '...':
atom = [OneOrMore(*atom)]
tokens.move()
result += atom
return result
def parse_atom(tokens, options):
"""atom ::= '(' expr ')' | '[' expr ']' | 'options'
| long | shorts | argument | command ;
"""
token = tokens.current()
result = []
if token in '([':
tokens.move()
matching, pattern = {'(': [')', Required], '[': [']', Optional]}[token]
result = pattern(*parse_expr(tokens, options))
if tokens.move() != matching:
raise tokens.error("unmatched '%s'" % token)
return [result]
elif token == 'options':
tokens.move()
return [OptionsShortcut()]
elif token.startswith('--') and token != '--':
return parse_long(tokens, options)
elif token.startswith('-') and token not in ('-', '--'):
return parse_shorts(tokens, options)
elif token.startswith('<') and token.endswith('>') or token.isupper():
return [Argument(tokens.move())]
else:
return [Command(tokens.move())]
def parse_argv(tokens, options, options_first=False):
"""Parse command-line argument vector.
If options_first:
argv ::= [ long | shorts ]* [ argument ]* [ '--' [ argument ]* ] ;
else:
argv ::= [ long | shorts | argument ]* [ '--' [ argument ]* ] ;
"""
parsed = []
while tokens.current() is not None:
if tokens.current() == '--':
return parsed + [Argument(None, v) for v in tokens]
elif tokens.current().startswith('--'):
parsed += parse_long(tokens, options)
elif tokens.current().startswith('-') and tokens.current() != '-':
parsed += parse_shorts(tokens, options)
elif options_first:
return parsed + [Argument(None, v) for v in tokens]
else:
parsed.append(Argument(None, tokens.move()))
return parsed
def parse_defaults(doc):
defaults = []
for s in parse_section('options:', doc):
# FIXME corner case "bla: options: --foo"
_, _, s = s.partition(':') # get rid of "options:"
split = re.split('\n[ \t]*(-\S+?)', '\n' + s)[1:]
split = [s1 + s2 for s1, s2 in zip(split[::2], split[1::2])]
options = [Option.parse(s) for s in split if s.startswith('-')]
defaults += options
return defaults
def parse_section(name, source):
pattern = re.compile('^([^\n]*' + name + '[^\n]*\n?(?:[ \t].*?(?:\n|$))*)',
re.IGNORECASE | re.MULTILINE)
return [s.strip() for s in pattern.findall(source)]
def formal_usage(section):
_, _, section = section.partition(':') # drop "usage:"
pu = section.split()
return '( ' + ' '.join(') | (' if s == pu[0] else s for s in pu[1:]) + ' )'
def extras(help, version, options, doc):
if help and any((o.name in ('-h', '--help')) and o.value for o in options):
print(doc.strip("\n"))
sys.exit()
if version and any(o.name == '--version' and o.value for o in options):
print(version)
sys.exit()
class Dict(dict):
def __repr__(self):
return '{%s}' % ',\n '.join('%r: %r' % i for i in sorted(self.items()))
def docopt(doc, argv=None, help=True, version=None, options_first=False):
"""Parse `argv` based on command-line interface described in `doc`.
`docopt` creates your command-line interface based on its
description that you pass as `doc`. Such description can contain
--options, <positional-argument>, commands, which could be
[optional], (required), (mutually | exclusive) or repeated...
Parameters
----------
doc : str
Description of your command-line interface.
argv : list of str, optional
Argument vector to be parsed. sys.argv[1:] is used if not
provided.
help : bool (default: True)
Set to False to disable automatic help on -h or --help
options.
version : any object
If passed, the object will be printed if --version is in
`argv`.
options_first : bool (default: False)
Set to True to require options precede positional arguments,
i.e. to forbid options and positional arguments intermix.
Returns
-------
args : dict
A dictionary, where keys are names of command-line elements
such as e.g. "--verbose" and "<path>", and values are the
parsed values of those elements.
Example
-------
>>> from docopt import docopt
>>> doc = '''
... Usage:
... my_program tcp <host> <port> [--timeout=<seconds>]
... my_program serial <port> [--baud=<n>] [--timeout=<seconds>]
... my_program (-h | --help | --version)
...
... Options:
... -h, --help Show this screen and exit.
... --baud=<n> Baudrate [default: 9600]
... '''
>>> argv = ['tcp', '127.0.0.1', '80', '--timeout', '30']
>>> docopt(doc, argv)
{'--baud': '9600',
'--help': False,
'--timeout': '30',
'--version': False,
'<host>': '127.0.0.1',
'<port>': '80',
'serial': False,
'tcp': True}
See also
--------
* For video introduction see http://docopt.org
* Full documentation is available in README.rst as well as online
at https://github.com/docopt/docopt#readme
"""
argv = sys.argv[1:] if argv is None else argv
usage_sections = parse_section('usage:', doc)
if len(usage_sections) == 0:
raise DocoptLanguageError('"usage:" (case-insensitive) not found.')
if len(usage_sections) > 1:
raise DocoptLanguageError('More than one "usage:" (case-insensitive).')
DocoptExit.usage = usage_sections[0]
options = parse_defaults(doc)
pattern = parse_pattern(formal_usage(DocoptExit.usage), options)
# [default] syntax for argument is disabled
#for a in pattern.flat(Argument):
# same_name = [d for d in arguments if d.name == a.name]
# if same_name:
# a.value = same_name[0].value
argv = parse_argv(Tokens(argv), list(options), options_first)
pattern_options = set(pattern.flat(Option))
for options_shortcut in pattern.flat(OptionsShortcut):
doc_options = parse_defaults(doc)
options_shortcut.children = list(set(doc_options) - pattern_options)
#if any_options:
# options_shortcut.children += [Option(o.short, o.long, o.argcount)
# for o in argv if type(o) is Option]
extras(help, version, argv, doc)
matched, left, collected = pattern.fix().match(argv)
if matched and left == []: # better error message if left?
return Dict((a.name, a.value) for a in (pattern.flat() + collected))
raise DocoptExit()
+76
View File
@@ -0,0 +1,76 @@
#!/usr/bin/env python3
# A script to invoke mkdocs with the correct environment.
# Additionally supports deploying via mike:
# ./mkdocs deploy [mike-deploy-options]
import errno, os, shutil, sys
from subprocess import call
support_dir = os.path.dirname(os.path.normpath(__file__))
build_dir = os.path.join(os.path.dirname(support_dir), 'build')
# Set PYTHONPATH for the mkdocstrings handler.
env = os.environ.copy()
path = env.get('PYTHONPATH')
env['PYTHONPATH'] = \
(path + ':' if path else '') + os.path.join(support_dir, 'python')
redirect_page = \
'''<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Redirecting</title>
<noscript>
<meta http-equiv="refresh" content="1; url=11.0/" />
</noscript>
<script>
window.location.replace(
"api/" + window.location.search + window.location.hash
);
</script>
</head>
<body>
Redirecting to <a href="api/">api</a>...
</body>
</html>
'''
config_path = os.path.join(support_dir, 'mkdocs.yml')
args = sys.argv[1:]
if len(args) > 0:
command = args[0]
if command == 'deploy':
git_url = 'https://github.com/' if 'CI' in os.environ else 'git@github.com:'
site_repo = git_url + 'fmtlib/fmt.dev.git'
site_dir = os.path.join(build_dir, 'fmt.dev')
try:
shutil.rmtree(site_dir)
except OSError as e:
if e.errno == errno.ENOENT:
pass
ret = call(['git', 'clone', '--depth=1', site_repo, site_dir])
if ret != 0:
sys.exit(ret)
# Copy the config to the build dir because the site is built relative to it.
config_build_path = os.path.join(build_dir, 'mkdocs.yml')
shutil.copyfile(config_path, config_build_path)
version = args[1]
ret = call(['mike'] + args + ['--config-file', config_build_path,
'--branch', 'master'], cwd=site_dir, env=env)
if ret != 0 or version == 'dev':
sys.exit(ret)
redirect_page_path = os.path.join(site_dir, version, 'api.html')
with open(redirect_page_path, "w") as file:
file.write(redirect_page)
ret = call(['git', 'add', redirect_page_path], cwd=site_dir)
if ret != 0:
sys.exit(ret)
ret = call(['git', 'commit', '--amend', '--no-edit'], cwd=site_dir)
sys.exit(ret)
elif not command.startswith('-'):
args += ['-f', config_path]
sys.exit(call(['mkdocs'] + args, env=env))
+48
View File
@@ -0,0 +1,48 @@
site_name: '{fmt}'
docs_dir: ../doc
repo_url: https://github.com/fmtlib/fmt
theme:
name: material
features:
- navigation.tabs
- navigation.top
- toc.integrate
extra_javascript:
- https://cdnjs.cloudflare.com/ajax/libs/highlight.js/10.7.2/highlight.min.js
- fmt.js
extra_css:
- https://cdnjs.cloudflare.com/ajax/libs/highlight.js/10.7.2/styles/default.min.css
- fmt.css
markdown_extensions:
- pymdownx.highlight:
# Use JavaScript syntax highlighter instead of Pygments because it
# automatically applies to code blocks extracted through Doxygen.
use_pygments: false
anchor_linenums: true
line_spans: __span
pygments_lang_class: true
- pymdownx.inlinehilite
- pymdownx.snippets
plugins:
- search
- mkdocstrings:
default_handler: cxx
nav:
- Home: index.md
- Get Started: get-started.md
- API: api.md
- Syntax: syntax.md
exclude_docs: ChangeLog-old.md
extra:
version:
provider: mike
generator: false
+201
View File
@@ -0,0 +1,201 @@
#!/usr/bin/env python3
# This script is based on
# https://github.com/rust-lang/rust/blob/master/library/core/src/unicode/printable.py
# distributed under https://github.com/rust-lang/rust/blob/master/LICENSE-MIT.
# This script uses the following Unicode tables:
# - UnicodeData.txt
from collections import namedtuple
import csv
import os
import subprocess
NUM_CODEPOINTS=0x110000
def to_ranges(iter):
current = None
for i in iter:
if current is None or i != current[1] or i in (0x10000, 0x20000):
if current is not None:
yield tuple(current)
current = [i, i + 1]
else:
current[1] += 1
if current is not None:
yield tuple(current)
def get_escaped(codepoints):
for c in codepoints:
if (c.class_ or "Cn") in "Cc Cf Cs Co Cn Zl Zp Zs".split() and c.value != ord(' '):
yield c.value
def get_file(f):
try:
return open(os.path.basename(f))
except FileNotFoundError:
subprocess.run(["curl", "-O", f], check=True)
return open(os.path.basename(f))
Codepoint = namedtuple('Codepoint', 'value class_')
def get_codepoints(f):
r = csv.reader(f, delimiter=";")
prev_codepoint = 0
class_first = None
for row in r:
codepoint = int(row[0], 16)
name = row[1]
class_ = row[2]
if class_first is not None:
if not name.endswith("Last>"):
raise ValueError("Missing Last after First")
for c in range(prev_codepoint + 1, codepoint):
yield Codepoint(c, class_first)
class_first = None
if name.endswith("First>"):
class_first = class_
yield Codepoint(codepoint, class_)
prev_codepoint = codepoint
if class_first is not None:
raise ValueError("Missing Last after First")
for c in range(prev_codepoint + 1, NUM_CODEPOINTS):
yield Codepoint(c, None)
def compress_singletons(singletons):
uppers = [] # (upper, # items in lowers)
lowers = []
for i in singletons:
upper = i >> 8
lower = i & 0xff
if len(uppers) == 0 or uppers[-1][0] != upper:
uppers.append((upper, 1))
else:
upper, count = uppers[-1]
uppers[-1] = upper, count + 1
lowers.append(lower)
return uppers, lowers
def compress_normal(normal):
# lengths 0x00..0x7f are encoded as 00, 01, ..., 7e, 7f
# lengths 0x80..0x7fff are encoded as 80 80, 80 81, ..., ff fe, ff ff
compressed = [] # [truelen, (truelenaux), falselen, (falselenaux)]
prev_start = 0
for start, count in normal:
truelen = start - prev_start
falselen = count
prev_start = start + count
assert truelen < 0x8000 and falselen < 0x8000
entry = []
if truelen > 0x7f:
entry.append(0x80 | (truelen >> 8))
entry.append(truelen & 0xff)
else:
entry.append(truelen & 0x7f)
if falselen > 0x7f:
entry.append(0x80 | (falselen >> 8))
entry.append(falselen & 0xff)
else:
entry.append(falselen & 0x7f)
compressed.append(entry)
return compressed
def print_singletons(uppers, lowers, uppersname, lowersname):
print(" static constexpr singleton {}[] = {{".format(uppersname))
for u, c in uppers:
print(" {{{:#04x}, {}}},".format(u, c))
print(" };")
print(" static constexpr unsigned char {}[] = {{".format(lowersname))
for i in range(0, len(lowers), 8):
print(" {}".format(" ".join("{:#04x},".format(l) for l in lowers[i:i+8])))
print(" };")
def print_normal(normal, normalname):
print(" static constexpr unsigned char {}[] = {{".format(normalname))
for v in normal:
print(" {}".format(" ".join("{:#04x},".format(i) for i in v)))
print(" };")
def main():
file = get_file("https://www.unicode.org/Public/UNIDATA/UnicodeData.txt")
codepoints = get_codepoints(file)
CUTOFF=0x10000
singletons0 = []
singletons1 = []
normal0 = []
normal1 = []
extra = []
for a, b in to_ranges(get_escaped(codepoints)):
if a > 2 * CUTOFF:
extra.append((a, b - a))
elif a == b - 1:
if a & CUTOFF:
singletons1.append(a & ~CUTOFF)
else:
singletons0.append(a)
elif a == b - 2:
if a & CUTOFF:
singletons1.append(a & ~CUTOFF)
singletons1.append((a + 1) & ~CUTOFF)
else:
singletons0.append(a)
singletons0.append(a + 1)
else:
if a >= 2 * CUTOFF:
extra.append((a, b - a))
elif a & CUTOFF:
normal1.append((a & ~CUTOFF, b - a))
else:
normal0.append((a, b - a))
singletons0u, singletons0l = compress_singletons(singletons0)
singletons1u, singletons1l = compress_singletons(singletons1)
normal0 = compress_normal(normal0)
normal1 = compress_normal(normal1)
print("""\
FMT_FUNC auto is_printable(uint32_t cp) -> bool {\
""")
print_singletons(singletons0u, singletons0l, 'singletons0', 'singletons0_lower')
print_singletons(singletons1u, singletons1l, 'singletons1', 'singletons1_lower')
print_normal(normal0, 'normal0')
print_normal(normal1, 'normal1')
print("""\
auto lower = static_cast<uint16_t>(cp);
if (cp < 0x10000) {
return is_printable(lower, singletons0,
sizeof(singletons0) / sizeof(*singletons0),
singletons0_lower, normal0, sizeof(normal0));
}
if (cp < 0x20000) {
return is_printable(lower, singletons1,
sizeof(singletons1) / sizeof(*singletons1),
singletons1_lower, normal1, sizeof(normal1));
}\
""")
for a, b in extra:
print(" if (0x{:x} <= cp && cp < 0x{:x}) return false;".format(a, a + b))
print("""\
return cp < 0x{:x};
}}\
""".format(NUM_CODEPOINTS))
if __name__ == '__main__':
main()
@@ -0,0 +1,338 @@
# A basic mkdocstrings handler for {fmt}.
# Copyright (c) 2012 - present, Victor Zverovich
# https://github.com/fmtlib/fmt/blob/master/LICENSE
import os
import xml.etree.ElementTree as ElementTree
from pathlib import Path
from subprocess import PIPE, STDOUT, CalledProcessError, Popen
from typing import Any, List, Mapping, Optional
from mkdocstrings.handlers.base import BaseHandler
class Definition:
"""A definition extracted by Doxygen."""
def __init__(self, name: str, kind: Optional[str] = None,
node: Optional[ElementTree.Element] = None,
is_member: bool = False):
self.name = name
self.kind = kind if kind is not None else node.get('kind')
self.desc = None
self.id = name if not is_member else None
self.members = None
self.params = None
self.template_params = None
self.trailing_return_type = None
self.type = None
# A map from Doxygen to HTML tags.
tag_map = {
'bold': 'b',
'emphasis': 'em',
'computeroutput': 'code',
'para': 'p',
'programlisting': 'pre',
'verbatim': 'pre'
}
# A map from Doxygen tags to text.
tag_text_map = {
'codeline': '',
'highlight': '',
'sp': ' '
}
def escape_html(s: str) -> str:
return s.replace("<", "&lt;")
def doxyxml2html(nodes: List[ElementTree.Element]):
out = ''
for n in nodes:
tag = tag_map.get(n.tag)
if not tag:
out += tag_text_map[n.tag]
out += '<' + tag + '>' if tag else ''
out += '<code class="language-cpp">' if tag == 'pre' else ''
if n.text:
out += escape_html(n.text)
out += doxyxml2html(list(n))
out += '</code>' if tag == 'pre' else ''
out += '</' + tag + '>' if tag else ''
if n.tail:
out += n.tail
return out
def convert_template_params(node: ElementTree.Element) -> Optional[List[Definition]]:
template_param_list = node.find('templateparamlist')
if template_param_list is None:
return None
params = []
for param_node in template_param_list.findall('param'):
name = param_node.find('declname')
param = Definition(name.text if name is not None else '', 'param')
param.type = param_node.find('type').text
params.append(param)
return params
def get_description(node: ElementTree.Element) -> List[ElementTree.Element]:
return node.findall('briefdescription/para') + \
node.findall('detaileddescription/para')
def normalize_type(type_: str) -> str:
type_ = type_.replace('< ', '<').replace(' >', '>')
return type_.replace(' &', '&').replace(' *', '*')
def convert_type(type_: ElementTree.Element) -> Optional[str]:
if type_ is None:
return None
result = type_.text if type_.text else ''
for ref in type_:
result += ref.text
if ref.tail:
result += ref.tail
result += type_.tail.strip()
return normalize_type(result)
def convert_params(func: ElementTree.Element) -> List[Definition]:
params = []
for p in func.findall('param'):
d = Definition(p.find('declname').text, 'param')
d.type = convert_type(p.find('type'))
params.append(d)
return params
def convert_return_type(d: Definition, node: ElementTree.Element) -> None:
d.trailing_return_type = None
if d.type == 'auto' or d.type == 'constexpr auto':
parts = node.find('argsstring').text.split(' -> ')
if len(parts) > 1:
d.trailing_return_type = normalize_type(parts[1])
def render_param(param: Definition) -> str:
return param.type + (f'&nbsp;{param.name}' if len(param.name) > 0 else '')
def render_decl(d: Definition) -> str:
text = ''
if d.id is not None:
text += f'<a id="{d.id}">\n'
text += '<pre><code class="language-cpp decl">'
text += '<div>'
if d.template_params is not None:
text += 'template &lt;'
text += ', '.join([render_param(p) for p in d.template_params])
text += '&gt;\n'
text += '</div>'
text += '<div>'
end = ';'
if d.kind == 'function' or d.kind == 'variable':
text += d.type + ' ' if len(d.type) > 0 else ''
elif d.kind == 'typedef':
text += 'using '
elif d.kind == 'define':
end = ''
else:
text += d.kind + ' '
text += d.name
if d.params is not None:
params = ', '.join([
(p.type + ' ' if p.type else '') + p.name for p in d.params])
text += '(' + escape_html(params) + ')'
if d.trailing_return_type:
text += ' -&NoBreak;>&nbsp;' + escape_html(d.trailing_return_type)
elif d.kind == 'typedef':
text += ' = ' + escape_html(d.type)
text += end
text += '</div>'
text += '</code></pre>\n'
if d.id is not None:
text += f'</a>\n'
return text
class CxxHandler(BaseHandler):
def __init__(self, **kwargs: Any) -> None:
super().__init__(handler='cxx', **kwargs)
headers = [
'args.h', 'base.h', 'chrono.h', 'color.h', 'compile.h', 'format.h',
'os.h', 'ostream.h', 'printf.h', 'ranges.h', 'std.h', 'xchar.h'
]
# Run doxygen.
cmd = ['doxygen', '-']
support_dir = Path(__file__).parents[3]
top_dir = os.path.dirname(support_dir)
include_dir = os.path.join(top_dir, 'include', 'fmt')
self._ns2doxyxml = {}
build_dir = os.path.join(top_dir, 'build')
os.makedirs(build_dir, exist_ok=True)
self._doxyxml_dir = os.path.join(build_dir, 'doxyxml')
p = Popen(cmd, stdin=PIPE, stdout=PIPE, stderr=STDOUT)
_, _ = p.communicate(input=r'''
PROJECT_NAME = fmt
GENERATE_XML = YES
GENERATE_LATEX = NO
GENERATE_HTML = NO
INPUT = {0}
XML_OUTPUT = {1}
QUIET = YES
AUTOLINK_SUPPORT = NO
MACRO_EXPANSION = YES
PREDEFINED = _WIN32=1 \
__linux__=1 \
FMT_ENABLE_IF(...)= \
FMT_USE_USER_LITERALS=1 \
FMT_USE_ALIAS_TEMPLATES=1 \
FMT_USE_NONTYPE_TEMPLATE_ARGS=1 \
FMT_API= \
"FMT_BEGIN_NAMESPACE=namespace fmt {{" \
"FMT_END_NAMESPACE=}}" \
"FMT_DOC=1"
'''.format(
' '.join([os.path.join(include_dir, h) for h in headers]),
self._doxyxml_dir).encode('utf-8'))
if p.returncode != 0:
raise CalledProcessError(p.returncode, cmd)
# Merge all file-level XMLs into one to simplify search.
self._file_doxyxml = None
for h in headers:
filename = h.replace(".h", "_8h.xml")
with open(os.path.join(self._doxyxml_dir, filename)) as f:
doxyxml = ElementTree.parse(f)
if self._file_doxyxml is None:
self._file_doxyxml = doxyxml
continue
root = self._file_doxyxml.getroot()
for node in doxyxml.getroot():
root.append(node)
def collect_compound(self, identifier: str,
cls: List[ElementTree.Element]) -> Definition:
"""Collect a compound definition such as a struct."""
path = os.path.join(self._doxyxml_dir, cls[0].get('refid') + '.xml')
with open(path) as f:
xml = ElementTree.parse(f)
node = xml.find('compounddef')
d = Definition(identifier, node=node)
d.template_params = convert_template_params(node)
d.desc = get_description(node)
d.members = []
for m in \
node.findall('sectiondef[@kind="public-attrib"]/memberdef') + \
node.findall('sectiondef[@kind="public-func"]/memberdef'):
name = m.find('name').text
# Doxygen incorrectly classifies members of private unnamed unions as
# public members of the containing class.
if name.endswith('_'):
continue
desc = get_description(m)
if len(desc) == 0:
continue
kind = m.get('kind')
member = Definition(name if name else '', kind=kind, is_member=True)
type_text = m.find('type').text
member.type = type_text if type_text else ''
if kind == 'function':
member.params = convert_params(m)
convert_return_type(member, m)
member.template_params = None
member.desc = desc
d.members.append(member)
return d
def collect(self, identifier: str, _config: Mapping[str, Any]) -> Definition:
qual_name = 'fmt::' + identifier
param_str = None
paren = qual_name.find('(')
if paren > 0:
qual_name, param_str = qual_name[:paren], qual_name[paren + 1:-1]
colons = qual_name.rfind('::')
namespace, name = qual_name[:colons], qual_name[colons + 2:]
# Load XML.
doxyxml = self._ns2doxyxml.get(namespace)
if doxyxml is None:
path = f'namespace{namespace.replace("::", "_1_1")}.xml'
with open(os.path.join(self._doxyxml_dir, path)) as f:
doxyxml = ElementTree.parse(f)
self._ns2doxyxml[namespace] = doxyxml
nodes = doxyxml.findall(
f"compounddef/sectiondef/memberdef/name[.='{name}']/..")
if len(nodes) == 0:
nodes = self._file_doxyxml.findall(
f"compounddef/sectiondef/memberdef/name[.='{name}']/..")
candidates = []
for node in nodes:
# Process a function or a typedef.
params = None
d = Definition(name, node=node)
if d.kind == 'function':
params = convert_params(node)
node_param_str = ', '.join([p.type for p in params])
if param_str and param_str != node_param_str:
candidates.append(f'{name}({node_param_str})')
continue
elif d.kind == 'define':
params = []
for p in node.findall('param'):
param = Definition(p.find('defname').text, kind='param')
param.type = None
params.append(param)
d.type = convert_type(node.find('type'))
d.template_params = convert_template_params(node)
d.params = params
convert_return_type(d, node)
d.desc = get_description(node)
return d
cls = doxyxml.findall(f"compounddef/innerclass[.='{qual_name}']")
if not cls:
raise Exception(f'Cannot find {identifier}. Candidates: {candidates}')
return self.collect_compound(identifier, cls)
def render(self, d: Definition, config: dict) -> str:
if d.id is not None:
self.do_heading('', 0, id=d.id)
text = '<div class="docblock">\n'
text += render_decl(d)
text += '<div class="docblock-desc">\n'
text += doxyxml2html(d.desc)
if d.members is not None:
for m in d.members:
text += self.render(m, config)
text += '</div>\n'
text += '</div>\n'
return text
def get_handler(theme: str, custom_templates: Optional[str] = None,
**_config: Any) -> CxxHandler:
"""Return an instance of `CxxHandler`.
Arguments:
theme: The theme to use when rendering contents.
custom_templates: Directory containing custom templates.
**_config: Configuration passed to the handler.
"""
return CxxHandler(theme=theme, custom_templates=custom_templates)
@@ -0,0 +1 @@
mkdocsstrings requires a handler to have a templates directory.
+188
View File
@@ -0,0 +1,188 @@
#!/usr/bin/env python3
"""Make a release.
Usage:
release.py [<branch>]
For the release command $FMT_TOKEN should contain a GitHub personal access token
obtained from https://github.com/settings/tokens.
"""
from __future__ import print_function
import datetime, docopt, errno, fileinput, json, os
import re, shutil, sys
from subprocess import check_call
import urllib.request
class Git:
def __init__(self, dir):
self.dir = dir
def call(self, method, args, **kwargs):
return check_call(['git', method] + list(args), **kwargs)
def add(self, *args):
return self.call('add', args, cwd=self.dir)
def checkout(self, *args):
return self.call('checkout', args, cwd=self.dir)
def clean(self, *args):
return self.call('clean', args, cwd=self.dir)
def clone(self, *args):
return self.call('clone', list(args) + [self.dir])
def commit(self, *args):
return self.call('commit', args, cwd=self.dir)
def pull(self, *args):
return self.call('pull', args, cwd=self.dir)
def push(self, *args):
return self.call('push', args, cwd=self.dir)
def reset(self, *args):
return self.call('reset', args, cwd=self.dir)
def update(self, *args):
clone = not os.path.exists(self.dir)
if clone:
self.clone(*args)
return clone
def clean_checkout(repo, branch):
repo.clean('-f', '-d')
repo.reset('--hard')
repo.checkout(branch)
class Runner:
def __init__(self, cwd):
self.cwd = cwd
def __call__(self, *args, **kwargs):
kwargs['cwd'] = kwargs.get('cwd', self.cwd)
check_call(args, **kwargs)
def create_build_env():
"""Create a build environment."""
class Env:
pass
env = Env()
env.fmt_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
env.build_dir = 'build'
env.fmt_repo = Git(os.path.join(env.build_dir, 'fmt'))
return env
if __name__ == '__main__':
args = docopt.docopt(__doc__)
env = create_build_env()
fmt_repo = env.fmt_repo
branch = args.get('<branch>')
if branch is None:
branch = 'master'
if not fmt_repo.update('-b', branch, 'git@github.com:fmtlib/fmt'):
clean_checkout(fmt_repo, branch)
# Update the date in the changelog and extract the version and the first
# section content.
changelog = 'ChangeLog.md'
changelog_path = os.path.join(fmt_repo.dir, changelog)
is_first_section = True
first_section = []
for i, line in enumerate(fileinput.input(changelog_path, inplace=True)):
if i == 0:
version = re.match(r'# (.*) - TBD', line).group(1)
line = '# {} - {}\n'.format(
version, datetime.date.today().isoformat())
elif not is_first_section:
pass
elif line.startswith('#'):
is_first_section = False
else:
first_section.append(line)
sys.stdout.write(line)
if first_section[0] == '\n':
first_section.pop(0)
ns_version = None
base_h_path = os.path.join(fmt_repo.dir, 'include', 'fmt', 'base.h')
for line in fileinput.input(base_h_path):
m = re.match(r'\s*inline namespace v(.*) .*', line)
if m:
ns_version = m.group(1)
break
major_version = version.split('.')[0]
if not ns_version or ns_version != major_version:
raise Exception(f'Version mismatch {ns_version} != {major_version}')
# Workaround GitHub-flavored Markdown treating newlines as <br>.
changes = ''
code_block = False
stripped = False
for line in first_section:
if re.match(r'^\s*```', line):
code_block = not code_block
changes += line
stripped = False
continue
if code_block:
changes += line
continue
if line == '\n' or re.match(r'^\s*\|.*', line):
if stripped:
changes += '\n'
stripped = False
changes += line
continue
if stripped:
line = ' ' + line.lstrip()
changes += line.rstrip()
stripped = True
fmt_repo.checkout('-B', 'release')
fmt_repo.add(changelog)
fmt_repo.commit('-m', 'Update version')
# Build the docs and package.
run = Runner(fmt_repo.dir)
run('cmake', '.')
run('make', 'doc', 'package_source')
# Create a release on GitHub.
fmt_repo.push('origin', 'release')
auth_headers = {'Authorization': 'token ' + os.getenv('FMT_TOKEN')}
req = urllib.request.Request(
'https://api.github.com/repos/fmtlib/fmt/releases',
data=json.dumps({'tag_name': version,
'target_commitish': 'release',
'body': changes, 'draft': True}).encode('utf-8'),
headers=auth_headers, method='POST')
with urllib.request.urlopen(req) as response:
if response.status != 201:
raise Exception(f'Failed to create a release ' +
'{response.status} {response.reason}')
response_data = json.loads(response.read().decode('utf-8'))
id = response_data['id']
# Upload the package.
uploads_url = 'https://uploads.github.com/repos/fmtlib/fmt/releases'
package = 'fmt-{}.zip'.format(version)
req = urllib.request.Request(
f'{uploads_url}/{id}/assets?name={package}',
headers={'Content-Type': 'application/zip'} | auth_headers,
data=open('build/fmt/' + package, 'rb').read(), method='POST')
with urllib.request.urlopen(req) as response:
if response.status != 201:
raise Exception(f'Failed to upload an asset '
'{response.status} {response.reason}')
short_version = '.'.join(version.split('.')[:-1])
check_call(['./mkdocs', 'deploy', short_version])