Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions .github/workflows/continuous.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ concurrency:
env:
CTEST_OUTPUT_ON_FAILURE: ON
CTEST_PARALLEL_LEVEL: 1
SCCACHE_GHA_ENABLED: "true"

jobs:
####################
Expand Down Expand Up @@ -198,6 +197,8 @@ jobs:
Windows:
name: windows-2025 (${{ matrix.config }})
runs-on: windows-2025
env:
SCCACHE_GHA_ENABLED: "true"
strategy:
fail-fast: false
matrix:
Expand Down Expand Up @@ -235,7 +236,7 @@ jobs:
id: cpu-cores

- name: Sccache
uses: mozilla-actions/sccache-action@v0.0.9
uses: mozilla-actions/sccache-action@v0.0.10

# We run configure + build in the same step, since they both need to call VsDevCmd
# Also, cmd uses ^ to break commands into multiple lines (in powershell this is `)
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/wheel.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ jobs:
uses: astral-sh/setup-uv@v5

- name: Build wheels
uses: pypa/cibuildwheel@v2.23.3
uses: pypa/cibuildwheel@v3.4.1
env:
CIBW_BUILD_FRONTEND: "build[uv]"
CIBW_ARCHS_LINUX: auto64
Expand Down
9 changes: 9 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,9 @@ uv.lock
*.jpg
*.jpeg

# Init files
*.ini

# Openvdb files
tensor_*.json
tensor_*.vdb
Expand Down Expand Up @@ -105,3 +108,9 @@ tags
.claude/agent-memory/
/.tmp/
.claude/settings.local.json

# Javascript bindings
bindings/js/adobe-lagrange-*.tgz
bindings/js/dist
bindings/js/node_modules
bindings/js/package-lock.json
51 changes: 42 additions & 9 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -50,32 +50,39 @@ endif()
# Set default compiler cache program based on available programs
find_program(SCCACHE_PROGRAM sccache)
find_program(CCACHE_PROGRAM ccache)
if(SCCACHE_PROGRAM)
set(LAGRANGE_CCACHE_PROGRAM "sccache" CACHE STRING "Compiler cache program to use (none, ccache or sccache)")
if(SCCACHE_PROGRAM AND CCACHE_PROGRAM)
# If both are available, we prefer sccache on Windows because it supports /Zi
if(WIN32)
set(LAGRANGE_CCACHE_PROGRAM_DEFAULT "sccache")
else()
set(LAGRANGE_CCACHE_PROGRAM_DEFAULT "ccache")
endif()
elseif(SCCACHE_PROGRAM)
set(LAGRANGE_CCACHE_PROGRAM_DEFAULT "sccache")
elseif(CCACHE_PROGRAM)
set(LAGRANGE_CCACHE_PROGRAM "ccache" CACHE STRING "Compiler cache program to use (none, ccache or sccache)")
set(LAGRANGE_CCACHE_PROGRAM_DEFAULT "ccache")
else()
set(LAGRANGE_CCACHE_PROGRAM "none" CACHE STRING "Compiler cache program to use (none, ccache or sccache)")
set(LAGRANGE_CCACHE_PROGRAM_DEFAULT "none")
endif()
set(LAGRANGE_CCACHE_PROGRAM ${LAGRANGE_CCACHE_PROGRAM_DEFAULT}
CACHE STRING "Compiler cache program to use (none, ccache or sccache)"
)

set(LAGRANGE_CCACHE_PROGRAMS none ccache sccache)
set_property(CACHE LAGRANGE_CCACHE_PROGRAM PROPERTY STRINGS ${LAGRANGE_CCACHE_PROGRAMS})

# Enable sscache if available
if((LAGRANGE_CCACHE_PROGRAM STREQUAL "sccache") AND SCCACHE_PROGRAM)
message(STATUS "Using sccache: ${SCCACHE_PROGRAM}")
set(CMAKE_MSVC_DEBUG_INFORMATION_FORMAT "$<$<CONFIG:Debug,RelWithDebInfo>:Embedded>")
foreach(lang IN ITEMS C CXX)
set(CMAKE_${lang}_COMPILER_LAUNCHER
${CMAKE_COMMAND} -E env ${ccacheEnv} ${SCCACHE_PROGRAM}
)
set(CMAKE_${lang}_COMPILER_LAUNCHER ${SCCACHE_PROGRAM})
endforeach()
endif()

# Enable ccache if available
if((LAGRANGE_CCACHE_PROGRAM STREQUAL "ccache") AND CCACHE_PROGRAM)
set(ccacheEnv
CCACHE_BASEDIR=${CMAKE_BINARY_DIR}
CCACHE_BASEDIR=${CMAKE_SOURCE_DIR}
CCACHE_SLOPPINESS=clang_index_store,include_file_ctime,include_file_mtime,locale,pch_defines,time_macros
)
message(STATUS "Using ccache: ${CCACHE_PROGRAM}")
Expand All @@ -102,6 +109,11 @@ if(LAGRANGE_TOPLEVEL_PROJECT AND NOT DEFINED CMAKE_TOOLCHAIN_FILE)
set(CMAKE_OSX_DEPLOYMENT_TARGET "13.0" CACHE STRING "Minimum OS X deployment version")
endif()

# Generates a `compile_commands.json` that can be used for autocompletion
if(LAGRANGE_TOPLEVEL_PROJECT)
set(CMAKE_EXPORT_COMPILE_COMMANDS ON CACHE BOOL "Enable/Disable output of compile commands during generation.")
endif()

################################################################################

if(SKBUILD)
Expand All @@ -124,6 +136,25 @@ project(Lagrange VERSION ${lagrange_version})

################################################################################

# sccache cannot cache /Zi unless each cl.exe writes to its own PDB.
# Replace the per-target PDB in CMake's compile rule with a per-object PDB.
if(MSVC AND (LAGRANGE_CCACHE_PROGRAM STREQUAL "sccache") AND SCCACHE_PROGRAM)
set(CMAKE_MSVC_DEBUG_INFORMATION_FORMAT "$<$<CONFIG:Debug,RelWithDebInfo>:ProgramDatabase>")
foreach(lang IN ITEMS C CXX)
# Match either '/Fd...' or '-Fd...' (CMake/Ninja uses the dash form).
string(REGEX REPLACE
"[-/]Fd<TARGET_COMPILE_PDB>"
"-Fd<OBJECT>.pdb"
CMAKE_${lang}_COMPILE_OBJECT
"${CMAKE_${lang}_COMPILE_OBJECT}")
# /FS is no longer required once PDBs aren't shared; harmless to leave,
# but you can drop it too:
string(REGEX REPLACE "[-/]FS( |$)" "" CMAKE_${lang}_COMPILE_OBJECT
"${CMAKE_${lang}_COMPILE_OBJECT}")
endforeach()
message(STATUS "Patched CXX rule: ${CMAKE_CXX_COMPILE_OBJECT}")
endif()

if(MINGW)
message(FATAL_ERROR "Please use the MSVC compiler on Windows")
endif()
Expand Down Expand Up @@ -370,6 +401,8 @@ include(lagrange_add_executable)
include(lagrange_add_module)
include(lagrange_add_performance)
include(lagrange_add_python_binding)
include(lagrange_add_js_binding)
include(lagrange_js_emit_registry)
include(lagrange_add_test)
include(lagrange_cpm_cache)
include(lagrange_download_data)
Expand Down
5 changes: 3 additions & 2 deletions LagrangeOptions.cmake.sample
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@
# Specify a custom install prefix path
# set(CMAKE_INSTALL_PREFIX ${CMAKE_SOURCE_DIR}/install CACHE STRING "Install directory used by install().")

# Generates a `compile_commands.json` that can be used for autocompletion
# set(CMAKE_EXPORT_COMPILE_COMMANDS ON CACHE BOOL "Enable/Disable output of compile commands during generation.")
# Whether to generates a `compile_commands.json` that can be used for autocompletion
# set(CMAKE_EXPORT_COMPILE_COMMANDS OFF CACHE BOOL "Enable/Disable output of compile commands during generation.")

# Use a specific C/C++ compiler, e.g. llvm-clang on macOS (so we can use clangd)
# set(CMAKE_C_COMPILER "/usr/local/opt/llvm/bin/clang" CACHE STRING "C compiler")
Expand Down Expand Up @@ -74,6 +74,7 @@
# option(LAGRANGE_MODULE_WINDING "Build module lagrange::winding" ON)

# General options
# option(LAGRANGE_BINDINGS_JS "Build JavaScript/WebAssembly bindings" OFF)
# option(LAGRANGE_COMPILE_TESTS "Enable compilation tests" ON)
# option(LAGRANGE_DOCUMENTATION "Build Doxygen documentation" ON)
# option(LAGRANGE_EXAMPLES "Build all examples" ON)
Expand Down
104 changes: 104 additions & 0 deletions cmake/lagrange/lagrange_add_js_binding.cmake
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
#
# Copyright 2026 Adobe. All rights reserved.
# This file is licensed to you under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License. You may obtain a copy
# of the License at http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software distributed under
# the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
# OF ANY KIND, either express or implied. See the License for the specific language
# governing permissions and limitations under the License.
#
function(lagrange_add_js_binding module_name module_type)
# module_name: JS key this binding exposes on `lagrange.<module_name>`. Usually matches
# the folder name; differs for renamed modules (e.g. serialization2 -> serialization).
# module_type: name of the TypeScript interface exported by ts/${module_name}.ts, used
# verbatim in the generated registry.ts (e.g. BVHModule, CoreModule, SerializationModule).
if(NOT module_name)
message(FATAL_ERROR "lagrange_add_js_binding() requires a JS module name argument")
endif()
if(NOT module_type)
message(FATAL_ERROR "lagrange_add_js_binding(${module_name}) requires a TS interface name")
endif()

# C++ library target name: folder name (parent of js/).
get_filename_component(module_path "${CMAKE_CURRENT_SOURCE_DIR}/.." REALPATH)
get_filename_component(cpp_target_name "${module_path}" NAME)

get_property(_initialized GLOBAL PROPERTY LAGRANGE_JS_BINDINGS_INITIALIZED)
if(NOT _initialized)
set_property(GLOBAL PROPERTY LAGRANGE_JS_BINDINGS_INITIALIZED TRUE)
get_property(lagrange_source_dir GLOBAL PROPERTY __lagrange_source_dir)
get_property(lagrange_binary_dir GLOBAL PROPERTY __lagrange_binary_dir)
add_subdirectory(
${lagrange_source_dir}/bindings/js
${lagrange_binary_dir}/bindings/lagrange_js)

# Clear any stale symlinks from a previous configure (e.g. renamed or
# removed ts/test files) so they don't get picked up by tsc/vitest.
# Only removes symlinks — preserves checked-in files like .gitignore.
file(GLOB _stale_module_links "${lagrange_source_dir}/bindings/js/src/modules/*.ts")
foreach(_f ${_stale_module_links})
if(IS_SYMLINK "${_f}")
file(REMOVE "${_f}")
endif()
endforeach()
file(GLOB _stale_test_links "${lagrange_source_dir}/bindings/js/test/*.ts")
foreach(_f ${_stale_test_links})
if(IS_SYMLINK "${_f}")
file(REMOVE "${_f}")
endif()
endforeach()
endif()

if(NOT TARGET lagrange_js)
return()
endif()

file(GLOB_RECURSE SRC_FILES "*.cpp" "*.h")
target_sources(lagrange_js PRIVATE ${SRC_FILES})
target_link_libraries(lagrange_js PRIVATE lagrange::${cpp_target_name})
target_include_directories(lagrange_js PRIVATE
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>)

# Symlink TypeScript files from modules/<name>/js/ into bindings/js/
get_property(lagrange_source_dir GLOBAL PROPERTY __lagrange_source_dir)
set(js_bindings_dir "${lagrange_source_dir}/bindings/js")

# Register into the generated registry. lagrange_js_emit_registry emits
# `bindings/js/src/generated/registry.ts` based on this list. Each registered module must
# ship a ts/${module_name}.ts file exporting `${module_type}` (interface) and `${module_name}ModuleKeys`.
set_property(GLOBAL APPEND PROPERTY LAGRANGE_JS_REGISTRY "${module_name}")
set_property(GLOBAL PROPERTY LAGRANGE_JS_REGISTRY_TYPE_${module_name} "${module_type}")

# Type files: modules/<name>/js/ts/*.ts → bindings/js/src/modules/*.ts
set(ts_dir "${CMAKE_CURRENT_SOURCE_DIR}/ts")
if(EXISTS "${ts_dir}")
if(NOT EXISTS "${ts_dir}/${module_name}.ts")
message(FATAL_ERROR
"lagrange_add_js_binding(${module_name}): expected ${ts_dir}/${module_name}.ts — "
"TS filename stem must match the JS module name.")
endif()
file(GLOB TS_FILES "${ts_dir}/*.ts")
foreach(ts_file ${TS_FILES})
get_filename_component(ts_name "${ts_file}" NAME)
set(link_path "${js_bindings_dir}/src/modules/${ts_name}")
if(NOT EXISTS "${link_path}")
file(CREATE_LINK "${ts_file}" "${link_path}" SYMBOLIC)
endif()
endforeach()
endif()

# Test files: modules/<name>/js/test/*.ts → bindings/js/test/*.ts
set(test_dir "${CMAKE_CURRENT_SOURCE_DIR}/test")
if(EXISTS "${test_dir}")
file(GLOB TEST_FILES "${test_dir}/*.ts")
foreach(test_file ${TEST_FILES})
get_filename_component(test_name "${test_file}" NAME)
set(link_path "${js_bindings_dir}/test/${test_name}")
if(NOT EXISTS "${link_path}")
file(CREATE_LINK "${test_file}" "${link_path}" SYMBOLIC)
endif()
endforeach()
endif()
endfunction()
2 changes: 1 addition & 1 deletion cmake/lagrange/lagrange_add_python_binding.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ function(lagrange_add_python_binding)
if (EXISTS ${CMAKE_CURRENT_LIST_DIR}/scripts)
message(STATUS "Adding scripts for module ${module_name}")
message(STATUS ${SKBUILD_SCRIPTS_DIR})
file(GLOB_RECURSE SCRIPTS ${CMAKE_CURRENT_LIST_DIR}/scripts/*)
file(GLOB_RECURSE SCRIPTS ${CMAKE_CURRENT_LIST_DIR}/scripts/*.py)
install(PROGRAMS ${SCRIPTS}
DESTINATION ${SKBUILD_SCRIPTS_DIR}
COMPONENT Lagrange_Python_Runtime
Expand Down
63 changes: 63 additions & 0 deletions cmake/lagrange/lagrange_js_emit_registry.cmake
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
#
# Copyright 2026 Adobe. All rights reserved.
# This file is licensed to you under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License. You may obtain a copy
# of the License at http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software distributed under
# the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
# OF ANY KIND, either express or implied. See the License for the specific language
# governing permissions and limitations under the License.
#
# Emit bindings/js/src/generated/registry.ts listing every module that registered via
# lagrange_add_js_binding(). Defined in root scope so cmake_language(DEFER CALL ...) can
# find it when firing at end of top-level directory processing.
function(lagrange_js_emit_registry)
get_property(_modules GLOBAL PROPERTY LAGRANGE_JS_REGISTRY)
if(NOT _modules)
return()
endif()
list(REMOVE_DUPLICATES _modules)
list(SORT _modules)

get_property(_src_dir GLOBAL PROPERTY __lagrange_source_dir)
set(_out "${_src_dir}/bindings/js/src/generated/registry.ts")

set(_content "// GENERATED FILE — do not edit. Regenerated by CMake (lagrange_js_emit_registry).\n")
string(APPEND _content "// One entry per modules/<name>/js/ts/<stem>.ts that was compiled into this build.\n\n")

foreach(_m IN LISTS _modules)
string(APPEND _content "export * from \"../modules/${_m}.js\";\n")
endforeach()
string(APPEND _content "\n")

foreach(_m IN LISTS _modules)
get_property(_type GLOBAL PROPERTY LAGRANGE_JS_REGISTRY_TYPE_${_m})
string(APPEND _content "import type { ${_type} } from \"../modules/${_m}.js\";\n")
string(APPEND _content "import { ${_m}ModuleKeys } from \"../modules/${_m}.js\";\n")
endforeach()
string(APPEND _content "\n")

string(APPEND _content "export interface Lagrange {\n")
foreach(_m IN LISTS _modules)
get_property(_type GLOBAL PROPERTY LAGRANGE_JS_REGISTRY_TYPE_${_m})
string(APPEND _content " ${_m}: ${_type};\n")
endforeach()
string(APPEND _content "}\n\n")

string(APPEND _content "export const moduleRegistry = {\n")
foreach(_m IN LISTS _modules)
string(APPEND _content " ${_m}: ${_m}ModuleKeys,\n")
endforeach()
string(APPEND _content "} as const;\n")

set(_existing "")
if(EXISTS "${_out}")
file(READ "${_out}" _existing)
endif()
if(NOT "${_existing}" STREQUAL "${_content}")
file(WRITE "${_out}" "${_content}")
list(LENGTH _modules _count)
message(STATUS "lagrange_js: wrote ${_out} (${_count} modules)")
endif()
endfunction()
4 changes: 2 additions & 2 deletions cmake/recipes/external/OpenVDB.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -138,8 +138,8 @@ function(openvdb_import_target)
ignore_package(ZLIB)
include(miniz)
if(NOT TARGET ZLIB::ZLIB)
get_target_property(_aliased miniz::miniz ALIASED_TARGET)
add_library(ZLIB::ZLIB ALIAS ${_aliased})
include(lagrange_alias_target)
lagrange_alias_target(ZLIB::ZLIB miniz::miniz)
endif()
endif()

Expand Down
4 changes: 2 additions & 2 deletions cmake/recipes/external/blosc.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -67,8 +67,8 @@ block()
ignore_package(ZLIB)
include(miniz)
if(NOT TARGET ZLIB::ZLIB)
get_target_property(_aliased miniz::miniz ALIASED_TARGET)
add_library(ZLIB::ZLIB ALIAS ${_aliased})
include(lagrange_alias_target)
lagrange_alias_target(ZLIB::ZLIB miniz::miniz)
endif()
set(ZLIB_INCLUDE_DIR "")
set(ZLIB_LIBRARY ZLIB::ZLIB)
Expand Down
8 changes: 7 additions & 1 deletion cmake/recipes/external/cista.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,17 @@ CPMAddPackage(
add_library(cista INTERFACE)
add_library(cista::cista ALIAS cista)

target_include_directories(cista SYSTEM INTERFACE "${cista_SOURCE_DIR}/include")
FetchContent_GetProperties(cista)
include(GNUInstallDirs)
target_include_directories(cista SYSTEM INTERFACE
$<BUILD_INTERFACE:${cista_SOURCE_DIR}/include>
$<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}>
)

set_target_properties(cista PROPERTIES FOLDER "third_party")

# Install rules
set(CMAKE_INSTALL_DEFAULT_COMPONENT_NAME cista)
install(DIRECTORY ${cista_SOURCE_DIR}/include/cista DESTINATION ${CMAKE_INSTALL_INCLUDEDIR})
install(TARGETS cista EXPORT Cista_Targets INCLUDES DESTINATION ${CMAKE_INSTALL_INCLUDEDIR})
install(EXPORT Cista_Targets DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/cista NAMESPACE cista::)
1 change: 1 addition & 0 deletions cmake/recipes/external/cpptrace.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ endif()
message(STATUS "Third-party (external): creating target 'cpptrace::cpptrace'")

lagrange_find_package(zstd CONFIG REQUIRED GLOBAL)
include(miniz)

include(CPM)
CPMAddPackage(
Expand Down
Loading
Loading