Hi all!

When I was starting using and developing Subversion, I had a big
challenge to build it for the first time, especially because
Subversion itself and its dependencies have different build systems,
so this is a very complicated process.

The build process can be improved by adding a generator for the CMake
build system. This can be easily implemented due to the extensibility
of the Subversion's gen-make.

Some advantages of using CMake in Subversion:
- CMake is used by some Subversion dependencies, so there is nothing
else to learn.
- Great Visual Studio integration for build and tests. In addition,
there is a VSCode extension for CMake.
- CMake provides a very useful and simple system for finding dependencies.
- Better for build scripts and reproducibility of the build.

The commands that would be needed to build Subversion, if you have all
dependencies installed:

```
$ .\gen-make.py -t cmake
$ cmake -B out -DCMAKE_INSTALL_PREFIX=/path/to/deps
$ cmake --build out
```

Additionally, there is a great tool, vcpkg, which can be used to build
dependencies. It is integrated with CMake. The following command can
be used to install dependencies:

```
git clone https://github.com/microsoft/vcpkg
cd vcpkg
.\bootstrap-vcpkg.bat -disableMetrics
.\vcpkg.exe install apr apr-util expat zlib
```

I am attaching a draft patch with the implementation of the CMake generator.

What do you think?

--
Timofei Zhakov
Index: build/generator/gen_cmake.py
===================================================================
--- build/generator/gen_cmake.py        (nonexistent)
+++ build/generator/gen_cmake.py        (working copy)
@@ -0,0 +1,199 @@
+import os
+import stat
+import sys
+from build.generator.gen_make import UnknownDependency, _normstr
+import ezt
+import gen_base
+
+class _eztdata(object):
+  def __init__(self, **kw):
+    vars(self).update(kw)
+
+class cmake_target():
+  def __init__(self, name: str, type: str, deps: str, libs: str,
+               sources, msvc_libs, msvc_export, msvc_static):
+    self.name = name
+    self.type = type
+    self.deps = deps
+    self.libs = libs
+    self.sources = sources
+    self.msvc_libs = msvc_libs
+    self.msvc_export = msvc_export
+    self.msvc_static = ezt.boolean(msvc_static)
+    self.has_msvc_libs = ezt.boolean(len(msvc_libs) > 0)
+
+def get_target_type(target: gen_base.Target):
+  if isinstance(target, gen_base.TargetExe):
+    if target.install == "test" or target.install == "sub-test":
+      return "test"
+    else:
+      return "exe"
+  if isinstance(target, gen_base.TargetSWIG):
+    return "swig"
+  if isinstance(target, gen_base.TargetSWIGProject):
+    return "swig-project"
+  if isinstance(target, gen_base.TargetSWIGLib):
+    return "swig-lib"
+  if isinstance(target, gen_base.TargetLib):
+    return "lib"
+  else:
+    return str(type(target))
+
+class Generator(gen_base.GeneratorBase):
+  _extension_map = {
+    ('exe', 'target'): '.exe',
+    ('exe', 'object'): '.obj',
+    ('lib', 'target'): '.dll',
+    ('lib', 'object'): '.obj',
+    ('pyd', 'target'): '.pyd',
+    ('pyd', 'object'): '.obj',
+    ('so', 'target'): '.so',
+    ('so', 'object'): '.obj',
+  }
+
+  def __init__(self, fname, verfname, options=None):
+    gen_base.GeneratorBase.__init__(self, fname, verfname, options)
+
+  def write(self):
+    install_deps = self.graph.get_deps(gen_base.DT_INSTALL)
+
+    install_sources = self.get_install_sources()
+
+    # ensure consistency between runs
+    install_deps.sort()
+
+    targets = []
+    ra_modules = []
+    fs_modules = []
+
+    for target_ob in self.graph.get_sources(gen_base.DT_LINK, "ra-libs"):
+      name = target_ob.name
+      if name == "libsvn_ra_serf":
+        pass
+      else:
+        ra_modules.append(target_ob.name)
+    for target_ob in self.graph.get_sources(gen_base.DT_LINK, "fs-libs"):
+      name = target_ob.name
+      if name == "libsvn_fs_base":
+        pass
+      else:
+        fs_modules.append(target_ob.name)
+
+    for target_ob in install_sources:
+      target_ob: gen_base.Target
+
+      if isinstance(target_ob, gen_base.TargetScript):
+        # there is nothing to build
+        continue
+
+      sources = []
+      deps = []
+      libs = []
+
+      for link_dep in self.graph.get_sources(gen_base.DT_LINK, target_ob.name):
+        if isinstance(link_dep, gen_base.TargetJava):
+          deps.append(link_dep.name)
+        elif isinstance(link_dep, gen_base.TargetLinked):
+          if link_dep.external_lib:
+            name = link_dep.name
+            if name == "ra-libs":
+              libs += ra_modules
+            elif name == "fs-libs":
+              libs += fs_modules
+            elif name in ["apriconv",
+                          "apr_memcache",
+                          "magic",
+                          "intl",
+                          "macos-plist",
+                          "macos-keychain",
+                          "sasl"]:
+              pass
+            else:
+              libs.append("external-" + name)
+          elif link_dep.external_project:
+            pass
+          else:
+            libs.append(link_dep.name)
+        elif isinstance(link_dep, gen_base.ObjectFile):
+          deps = self.graph.get_sources(gen_base.DT_OBJECT,
+                                        link_dep,
+                                        gen_base.SourceFile)
+          for dep in deps:
+            sources.append(dep.filename)
+        elif isinstance(link_dep, gen_base.HeaderFile):
+          pass
+        else:
+          raise UnknownDependency
+
+      target_type = get_target_type(target_ob)
+      target_name = target_ob.name
+
+      if target_type == "exe" or target_type == "lib" or target_type == "test":
+        msvc_libs = []
+        for lib in target_ob.msvc_libs:
+          if lib == "setargv.obj":
+            # TODO:
+            pass
+          else:
+            msvc_libs.append(lib)
+
+        msvc_export = []
+        msvc_static = False
+        if isinstance(target_ob, gen_base.TargetLib):
+          for export in target_ob.msvc_export:
+            path = "subversion/include/" + export.replace("\\", "/")
+            msvc_export.append(path)
+          msvc_static = target_ob.msvc_static
+
+        targets.append(cmake_target(target_name, target_type,
+                                    deps, libs, sources,
+                                    msvc_libs, msvc_export, msvc_static))
+
+    data = _eztdata(
+      targets = targets,
+    )
+
+    template = ezt.Template(os.path.join('build', 'generator', 'templates',
+                                         'CMakeLists.txt.ezt'),
+                            compress_whitespace=False)
+    template.generate(open('CMakeLists.txt', 'w'), data)
+
+  def get_install_sources(self):
+    install_sources = self.graph.get_all_sources(gen_base.DT_INSTALL)
+    result = []
+
+    for target in install_sources:
+      target: gen_base.Target
+
+      if not self.check_ignore_target(target):
+        result.append(target)
+
+    result.sort(key = lambda s: s.name)
+
+    return result
+
+  def check_ignore_target(self, target: gen_base.Target):
+    ignore_names = [
+      "libsvn_fs_base",
+      "libsvn_auth_gnome_keyring",
+      "libsvn_auth_kwallet",
+      "libsvn_ra_serf",
+      "libsvnxx",
+      "svnxx-tests",
+      "mod_dav_svn",
+      "mod_authz_svn",
+      "mod_dontdothat",
+      "__JAVAHL__",
+      "__JAVAHL_TESTS__",
+      "libsvnjavahl",
+    ]
+
+    for name in ignore_names:
+      if target.name == name:
+        return True
+
+      if isinstance(target, gen_base.TargetExe):
+        if target.install == "bdb-test":
+          return True
+
+    return False

Property changes on: build/generator/gen_cmake.py
___________________________________________________________________
Added: svn:eol-style
## -0,0 +1 ##
+native
\ No newline at end of property
Index: build/generator/templates/CMakeLists.txt.ezt
===================================================================
--- build/generator/templates/CMakeLists.txt.ezt        (nonexistent)
+++ build/generator/templates/CMakeLists.txt.ezt        (working copy)
@@ -0,0 +1,148 @@
+cmake_minimum_required(VERSION 3.5)
+
+project("Subversion")
+
+option(SVN_BUILD_LIBRARIES "Build Subversion libraries" ON)
+option(SVN_BUILD_PROGRAMS "Build Subversion programs (such as svn.exe)" ON)
+option(SVN_BUILD_TEST "Build Subversion test-suite" OFF)
+
+option(BUILD_SHARED_LIBS "Build using shared libraries" OFF)
+option(SVN_SQLITE_USE_AMALGAMATION "Use sqlite amalgamation" ON)
+set(SVN_SQLITE_AMALGAMATION_DIR "${CMAKE_SOURCE_DIR}/sqlite-amalgamation"
+    CACHE STRING "Directory with sqlite amalgamation"
+)
+option(SVN_USE_INTERNAL_UTF8PROC "Use internal version of utf8proc" ON)
+option(SVN_USE_INTERNAL_LZ4 "Use internal version of lz4" ON)
+
+if (NOT SVN_BUILD_LIBRARIES AND (SVN_BUILD_PROGRAMS OR SVN_BUILD_TEST))
+  message(FATAL_ERROR "Libraries are required for programs or tests.
+Please enable SVN_BUILD_LIBRARIES")
+endif()
+
+find_package(Python COMPONENTS Interpreter REQUIRED)
+
+configure_file(
+  "subversion/svn_private_config.hw"
+  "svn_private_config.h"
+)
+
+set(SVN_INCLUDE_DIRECTORIES
+  "${CMAKE_CURRENT_SOURCE_DIR}/subversion/include"
+  "${CMAKE_CURRENT_BINARY_DIR}"
+)
+
+add_compile_definitions(
+  "alloca=_alloca"
+)
+
+find_package(apr)
+
+# apr-2 (from trunk)
+if(TARGET apr::libapr-2)
+  add_library(external-apr ALIAS apr::libapr-2)
+  add_library(external-aprutil ALIAS apr::libapr-2)
+elseif(TARGET apr::apr-2)
+  add_library(external-apr ALIAS apr::apr-2)
+  add_library(external-aprutil ALIAS apr::apr-2)
+
+# apr-1 (doesn't work, because apr-util doesn't provide CMake targets.)
+# TODO:
+elseif(TARGET apr::libapr-1 AND apr::libaprutil-1)
+  add_library(external-apr ALIAS apr::libapr-1)
+  add_library(external-aprutil ALIAS apr::libaprutil-1)
+elseif(TARGET apr::apr-1 AND apr::aprutil-1)
+  add_library(external-apr ALIAS apr::apr-1)
+  add_library(external-aprutil ALIAS apr::aprutil-1)
+
+# vcpkg (doesn't work, because apr-util doesn't provide CMake targets.)
+elseif(TARGET unofficial::apr::libapr-1 AND unofficial::apr::libaprutil-1)
+  add_library(external-apr ALIAS unofficial::apr::libapr-1)
+  add_library(external-aprutil ALIAS unofficial::apr::libaprutil-1)
+elseif(TARGET unofficial::apr::apr-1 AND unofficial::apr::aprutil-1)
+  add_library(external-apr ALIAS unofficial::apr::apr-1)
+  add_library(external-aprutil ALIAS unofficial::apr::aprutil-1)
+endif()
+
+find_package(ZLIB)
+add_library(external-zlib ALIAS ZLIB::ZLIB)
+
+find_package(expat CONFIG REQUIRED)
+add_library(external-xml ALIAS expat::expat)
+
+if(SVN_USE_INTERNAL_LZ4)
+  add_library(external-lz4 STATIC "build/win32/empty.c")
+  target_compile_definitions(external-lz4 PUBLIC "SVN_INTERNAL_LZ4")
+else()
+  find_package(lz4 CONFIG REQUIRED)
+  add_library(external-lz4 ALIAS lz4::lz4)
+endif()
+
+if(SVN_USE_INTERNAL_UTF8PROC)
+  add_library(external-utf8proc STATIC "build/win32/empty.c")
+  target_compile_definitions(external-utf8proc PUBLIC "SVN_INTERNAL_UTF8PROC")
+else()
+  # TODO:
+  # find_package(utf8proc CONFIG REQUIRED)
+  # add_library(external-utf8proc ALIAS utf8proc)
+endif()
+
+if(SVN_SQLITE_USE_AMALGAMATION)
+  add_library(external-sqlite STATIC "build/win32/empty.c")
+  target_include_directories(external-sqlite
+                             PUBLIC "${SVN_SQLITE_AMALGAMATION_DIR}")
+  target_compile_definitions(external-sqlite PUBLIC SVN_SQLITE_INLINE)
+else()
+  find_package(SQLite3 REQUIRED)
+  add_library(external-sqlite ALIAS SQLite::SQLite3)
+endif()
+
+install(DIRECTORY "subversion/include/" DESTINATION "include/subversion-1")
+[for targets][is targets.type "exe"]
+if (SVN_BUILD_PROGRAMS)
+  add_executable([targets.name][for targets.sources]
+    [targets.sources][end]
+  )
+  install(TARGETS [targets.name])
+  [end][is targets.type "lib"]
+if (SVN_BUILD_LIBRARIES)
+  add_library([targets.name][if-any targets.msvc_static] STATIC[end][for 
targets.sources]
+    [targets.sources][end]
+  )
+  if(BUILD_SHARED_LIBS)
+    set([targets.name]_HEADERS[for targets.msvc_export]
+      "[targets.msvc_export]"[end]
+    )
+    add_custom_command(
+      WORKING_DIRECTORY
+        ${CMAKE_SOURCE_DIR}
+      COMMAND
+        ${Python_EXECUTABLE}
+      ARGS
+        "build/generator/extractor.py"
+        ${[targets.name]_HEADERS}
+        ">${CMAKE_BINARY_DIR}/[targets.name].def"
+      OUTPUT
+        "${CMAKE_BINARY_DIR}/[targets.name].def"
+      DEPENDS
+        "build/generator/extractor.py"
+        ${[targets.name]_HEADERS}
+    )
+    target_sources([targets.name] PRIVATE 
"${CMAKE_BINARY_DIR}/[targets.name].def")
+  endif()
+  target_include_directories([targets.name]
+    PUBLIC ${SVN_INCLUDE_DIRECTORIES}
+  )
+  install(TARGETS [targets.name])
+[end][is targets.type "test"]
+if(SVN_BUILD_TEST)
+  add_executable([targets.name][for targets.sources] [targets.sources][end])
+  add_test([targets.name] [targets.name])
+[end]  target_link_libraries([targets.name] PRIVATE[for targets.libs]
+    [targets.libs][end]
+  )[if-any targets.has_msvc_libs]
+  if(WIN32)
+    target_link_libraries([targets.name] PRIVATE[for targets.msvc_libs] 
[targets.msvc_libs][end])
+  endif()[end]
+endif()
+[end]
+enable_testing()
Index: gen-make.py
===================================================================
--- gen-make.py (revision 1918171)
+++ gen-make.py (working copy)
@@ -49,6 +49,7 @@ sys.path.insert(1, 'build')
 gen_modules = {
   'make' : ('gen_make', 'Makefiles for POSIX systems'),
   'vcproj' : ('gen_vcnet_vcproj', 'VC.Net project files'),
+  'cmake' : ('gen_cmake', 'CMake build system'),
   }
 
 def main(fname, gentype, verfname=None,
Index: .
===================================================================
--- .   (revision 1918171)
+++ .   (working copy)

Property changes on: .
___________________________________________________________________
Modified: svn:ignore
## -65,3 +65,5 ##
 .swig_pl_checked
 .swig_py_checked
 .swig_rb_checked
+out
+CMakeLists.txt

Reply via email to