Skip to content

Commit 5719862

Browse files
authored
Merge pull request #19 from Garcia6l20/feature/cmake
Feature/cmake
2 parents b713270 + 1a3c105 commit 5719862

30 files changed

+804
-384
lines changed

.github/workflows/main.yaml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,8 +46,7 @@ jobs:
4646
with_conan: false
4747
targets: |
4848
libraries.use-simple-lib \
49-
dan.io.test-mbedtls \
50-
dan.io.UseCatch2
49+
dan-examples.cxx.dan.io.test-spdlog
5150
5251
name: ${{ matrix.config.name }}
5352
runs-on: ${{ matrix.config.os }}
@@ -68,6 +67,7 @@ jobs:
6867
# while fmt (built within examples) does not compile with defaul msys-gcc :)
6968
install: >-
7069
mingw-w64-x86_64-gcc
70+
mingw-w64-x86_64-make
7171
7272
- name: Set up Python 3.11
7373
uses: actions/setup-python@v3
@@ -98,6 +98,7 @@ jobs:
9898
- name: Configure
9999
run: |
100100
cd examples
101+
dan-io configure -s github.api_token=${{ secrets.GITHUB_TOKEN }}
101102
dan configure -v \
102103
-t default \
103104
-s install.destination=dist \

dan/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import sys
2+
import dan.logging
23
import dan.core.include
34
from dan.core.include import include, requires
45
from dan.core.generator import generator

dan/cli/click.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
import dataclasses
22
from enum import Enum
33
import types as typs
4-
import typing as t
54
from click import *
65

76
import inspect
87
import asyncio
98

9+
import dan.core.typing as t
1010
from dan import logging
1111

1212
class AsyncContext(Context):
@@ -49,6 +49,8 @@ def gen_comps(fields, parts: list[str], prefix=''):
4949
type = field.type
5050
if isinstance(type, typs.GenericAlias):
5151
type = t.get_origin(type)
52+
elif t.is_optional(type):
53+
type = t.get_args(type)[0]
5254
if dataclasses.is_dataclass(type):
5355
subfields = dataclasses.fields(type)
5456
subparts = parts[1:]

dan/cli/io.py

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,21 @@
66

77
from dan.cli import click
88
from dan.core.requirements import parse_package
9+
from dan.core.cache import Cache
10+
from dan.io.repositories import RepositoriesSettings, _get_settings
911
from dan.make import Make
1012

13+
def get_source_path():
14+
from dan.cxx.detect import get_dan_path
15+
source_path = get_dan_path() / 'deps'
16+
source_path.mkdir(exist_ok=True, parents=True)
17+
return source_path
18+
1119
_make : Make = None
1220
async def get_make(toolchain='default', quiet=True):
1321
global _make
1422
if _make is None:
15-
from dan.cxx.detect import get_dan_path
16-
source_path = get_dan_path() / 'deps'
17-
source_path.mkdir(exist_ok=True, parents=True)
23+
source_path = get_source_path()
1824
os.chdir(source_path)
1925
(source_path / 'dan-build.py').touch()
2026
make = Make(source_path / 'build', quiet=quiet)
@@ -58,6 +64,14 @@ async def make_context(toolchain='default', quiet=True):
5864
def cli():
5965
pass
6066

67+
@cli.command()
68+
@click.option('--setting', '-s', 'settings', type=click.SettingsParamType(RepositoriesSettings), multiple=True)
69+
async def configure(settings):
70+
io_settings = _get_settings()
71+
from dan.core.settings import apply_settings
72+
apply_settings(io_settings, *settings, logger=click.logger)
73+
await Cache.save_all()
74+
6175
@cli.group()
6276
def ls():
6377
"""Inspect stuff"""
@@ -115,6 +129,19 @@ async def versions(library: str):
115129
else:
116130
click.echo(f' - {v}')
117131

132+
@ls.command()
133+
@click.argument('LIBRARY')
134+
async def options(library: str):
135+
"""Get LIBRARY's available options"""
136+
async with make_context():
137+
lib = await get_library(library)
138+
await lib.initialize()
139+
for o in lib.options:
140+
current = ''
141+
if o.value != o.default:
142+
current = f', current: {o.value}'
143+
click.echo(f'{o.name}: {o.help} (type: {o.type.__name__}, default: {o.default}{current})')
144+
118145
@cli.command()
119146
@click.argument('NAME')
120147
async def search(name):

dan/cli/main.py

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ def cli(ctx: click.AsyncContext, **kwds):
9696
@click.option('--verbose', '-v', is_flag=True,
9797
help='Pring debug informations')
9898
@click.option('--toolchain', '-t', help='The toolchain to use',
99-
type=click.ToolchainParamType())
99+
type=click.ToolchainParamType(), envvar='DAN_TOOLCHAIN')
100100
@click.option('--setting', '-s', 'settings', help='Set or change a setting', multiple=True, type=click.SettingsParamType(Settings))
101101
@click.option('--option', '-o', 'options', help='Set or change an option', multiple=True, type=click.OptionsParamType())
102102
@click.option('--build-path', '-B', help='Path where dan has been initialized.',
@@ -108,7 +108,9 @@ async def configure(ctx: CommandsContext, toolchain: str, settings: tuple[str],
108108
"""Configure dan project"""
109109
ctx(**kwds) # update kwds
110110
if toolchain is None and ctx.make.config.toolchain is None:
111-
toolchain = click.prompt('Toolchain', type=click.ToolchainParamType(), default='default')
111+
from dan.cxx.detect import get_toolchains
112+
tp = click.Choice(get_toolchains(create=False)["toolchains"].keys())
113+
toolchain = click.prompt('Toolchain', type=tp, default='default')
112114

113115
await ctx.make.configure(source_path, toolchain)
114116

@@ -136,9 +138,15 @@ async def build(ctx: CommandsContext, force=False, **kwds):
136138
if force:
137139
await ctx.make.clean()
138140
await ctx.make.build()
139-
# from dan.cxx import target_toolchain
140-
# target_toolchain.compile_commands.update()
141141

142+
@cli.command()
143+
@common_opts
144+
@click.argument('TARGETS', nargs=-1, type=click.TargetParamType())
145+
@pass_context
146+
async def install_dependencies(ctx: CommandsContext, **kwds):
147+
"""Build targets"""
148+
ctx(**kwds) # update kwds
149+
await ctx.make.install_dependencies()
142150

143151
@cli.command()
144152
@common_opts

dan/cmake/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
1+
from dan.cmake.project import Project
12
from dan.cmake.configure_file import ConfigureFile

dan/cmake/project.py

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
from dan.core.requirements import RequiredPackage
2+
from dan.core.target import Target, FileDependency, Installer
3+
from dan.core.runners import async_run
4+
from dan.core.pm import re_match
5+
from dan.core import aiofiles
6+
from dan.core.find import find_executable, find_file
7+
from dan.cxx import Toolchain
8+
9+
import typing as t
10+
11+
12+
class Project(Target, internal=True):
13+
14+
cmake_targets: list[str] = None
15+
cmake_config_definitions: dict[str, str] = dict()
16+
cmake_patch_debug_postfix: list = None
17+
cmake_options: dict[str, tuple[str, t.Any, str]] = None
18+
19+
def __init__(self, *args, **kwargs):
20+
super().__init__(*args, **kwargs)
21+
self.cmake_cache_dep = FileDependency(self.build_path / 'CMakeCache.txt')
22+
self.dependencies.add(self.cmake_cache_dep)
23+
self.toolchain : Toolchain = self.context.get('cxx_target_toolchain')
24+
25+
async def _cmake(self, *cmake_args, **kwargs):
26+
return await async_run(['cmake', *cmake_args], logger=self, cwd=self.build_path, **kwargs, env=self.toolchain.env)
27+
28+
@property
29+
def _target_args(self):
30+
targets_args = []
31+
if self.cmake_targets is not None:
32+
for target in self.cmake_targets:
33+
targets_args.extend(('-t', target))
34+
return targets_args
35+
36+
async def __initialize__(self):
37+
if self.cmake_options is not None:
38+
for name, (cmake_name, default, help) in self.cmake_options.items():
39+
opt = self.options.add(name, default, help)
40+
setattr(opt, 'cmake_name', cmake_name)
41+
return await super().__initialize__()
42+
43+
async def __build__(self):
44+
cmake_options = dict()
45+
for opt in self.options:
46+
if hasattr(opt, 'cmake_name'):
47+
value = opt.value
48+
if isinstance(value, bool):
49+
value = 'ON' if value else 'OFF'
50+
cmake_options[opt.cmake_name] = value
51+
52+
cmake_options['CMAKE_PREFIX_PATH'] = self.makefile.root.pkgs_path.as_posix()
53+
54+
base_opts = []
55+
if self.toolchain.system.startswith('msys'):
56+
make = find_executable(r'.+make', self.toolchain.env['PATH'].split(';'), default_paths=False)
57+
base_opts.extend((f'-GMinGW Makefiles', f'-DCMAKE_MAKE_PROGRAM={make.as_posix()}'))
58+
59+
await self._cmake(
60+
self.source_path,
61+
*base_opts,
62+
f'-DCMAKE_BUILD_TYPE={self.toolchain.build_type.name.upper()}',
63+
f'-DCMAKE_CONFIGURATION_TYPES={self.toolchain.build_type.name.upper()}',
64+
f'-DCMAKE_C_COMPILER={self.toolchain.cc.as_posix()}',
65+
f'-DCMAKE_CXX_COMPILER={self.toolchain.cxx.as_posix()}',
66+
*[f'-D{k}={v}' for k, v in self.cmake_config_definitions.items()],
67+
*[f'-D{k}={v}' for k, v in cmake_options.items()]
68+
)
69+
await self._cmake('--build', '.', '--parallel', *self._target_args)
70+
71+
async def __install__(self, installer: Installer):
72+
await self.build()
73+
await self._cmake('.', f'-DCMAKE_INSTALL_PREFIX={installer.settings.destination}')
74+
await self._cmake('--install', '.', *self._target_args)
75+
await super().__install__(installer)
76+
async with aiofiles.open(self.build_path / 'install_manifest.txt') as manifest_file:
77+
manifest = await manifest_file.readlines()
78+
79+
if self.cmake_patch_debug_postfix is not None and self.toolchain.build_type.is_debug_mode:
80+
# fix: no 'd' postfix in MSVC pkgconfig
81+
seach_paths = [
82+
installer.settings.data_destination / 'pkgconfig',
83+
installer.settings.libraries_destination / 'pkgconfig',
84+
]
85+
for provided in self.provides:
86+
pc_file = find_file(rf'{provided}\.pc', paths=seach_paths)
87+
self.debug('patching %s', pc_file)
88+
for name in self.cmake_patch_debug_postfix:
89+
await aiofiles.sub(pc_file, rf'-l{name}(\s)', rf'-l{name}d\g<1>')
90+
91+
installer.installed_files.extend(manifest)

dan/core/aiofiles.py

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,11 @@
44

55
import os as sync_os
66
import stat
7+
import re
8+
import sys
9+
import errno
10+
import contextlib
11+
import time
712

813
from dan.core.pathlib import Path
914

@@ -49,3 +54,75 @@ async def copy(src : Path, dest : Path, chunk_size=2048):
4954
break
5055
await d.write(chunk)
5156
dest.chmod(src.stat().st_mode)
57+
58+
59+
async def sub(filepath, pattern, repl, **kwargs):
60+
async with open(filepath) as f:
61+
content = await f.read()
62+
content = re.sub(pattern, repl, content, **kwargs)
63+
async with open(filepath, 'w') as f:
64+
await f.write(content)
65+
66+
67+
68+
class FileLock:
69+
def __init__(self, path: str|Path, timeout=None, poll_interval=0.1) -> None:
70+
self._path = Path(path)
71+
self._mode: int = 0o644
72+
self._fh = None
73+
self._timeout = timeout
74+
self._poll_interval = poll_interval
75+
76+
def __del__(self):
77+
if self.has_lock:
78+
self.release()
79+
80+
@property
81+
def locked(self):
82+
return self._path.exists()
83+
84+
@property
85+
def has_lock(self):
86+
return self._fh is not None
87+
88+
def try_acquire(self):
89+
flags = (
90+
sync_os.O_WRONLY # open for writing only
91+
| sync_os.O_CREAT
92+
| sync_os.O_EXCL # together with above raise EEXIST if the file specified by filename exists
93+
| sync_os.O_TRUNC # truncate the file to zero byte
94+
)
95+
try:
96+
self._fh = sync_os.open(self._path, flags, self._mode)
97+
return True
98+
except OSError as exception: # re-raise unless expected exception
99+
if not (
100+
exception.errno == errno.EEXIST # lock already exist
101+
or (exception.errno == errno.EACCES and sys.platform == "win32") # has no access to this lock
102+
): # pragma: win32 no cover
103+
raise
104+
return False
105+
106+
async def acquire(self, timeout=None):
107+
if timeout is None:
108+
timeout = self._timeout
109+
t0 = t1 = time.perf_counter()
110+
while timeout is None or timeout < t1 - t0:
111+
if self.try_acquire():
112+
return True
113+
await asyncio.sleep(self._poll_interval)
114+
t1 = time.perf_counter()
115+
return False
116+
117+
def release(self):
118+
assert self._fh is not None
119+
sync_os.close(self._fh)
120+
self._fh = None
121+
with contextlib.suppress(OSError): # the file is already deleted and that's what we want
122+
self._path.unlink()
123+
124+
async def __aenter__(self):
125+
await self.acquire()
126+
127+
async def __aexit__(self, *exc):
128+
self.release()

dan/core/cache.py

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -128,4 +128,35 @@ def wrapper(self, *args, **kwds):
128128

129129
return wrapper
130130

131-
131+
class _CachedProperty:
132+
def __init__(self, getter: t.Callable[[], t.Any], cache_name=None):
133+
self.__getter = getter
134+
self.__name = getter.__name__
135+
self.__cache_name = 'cache' if cache_name is None else cache_name
136+
137+
def __get__(self, instance, owner: type | None = None):
138+
cache = getattr(instance, self.__cache_name)
139+
if isinstance(cache, Cache):
140+
cache = cache.data
141+
value = cache.get(self.__name)
142+
if value is None:
143+
value = self.__getter(instance)
144+
if value is not None:
145+
cache[self.__name] = value
146+
return value
147+
148+
def __set__(self, instance, value):
149+
cache = getattr(instance, self.__cache_name)
150+
if isinstance(cache, Cache):
151+
cache = cache.data
152+
cache[self.__name] = value
153+
154+
def __delete__(self, instance):
155+
cache = getattr(instance, self.__cache_name)
156+
del cache[self.__name]
157+
158+
159+
def cached_property(cache_name=None):
160+
def wrapper(fn):
161+
return _CachedProperty(fn, cache_name=cache_name)
162+
return wrapper

0 commit comments

Comments
 (0)