allow optional URLs on dependencies

main
Joca 2025-10-16 11:37:10 -03:00
parent 0783e68b0a
commit abb2315341
Signed by: jocadbz
GPG Key ID: B1836DCE2F50BDF7
2 changed files with 119 additions and 96 deletions

17
DOCS.md
View File

@ -155,17 +155,30 @@ lana setup
``` ```
Fetches and extracts external dependencies declared in `config.ini` under `[dependencies]` sections. Each dependency supports the following keys: Fetches and extracts external dependencies declared in `config.ini` under `[dependencies]` sections. Each dependency supports the following keys:
- `name` - logical name for the dependency - `name` - logical name for the dependency (required)
- `url` - download URL or git repository - `url` - download URL or git repository (optional)
- `archive` - optional filename to save the downloaded archive under `dependencies/tmp` - `archive` - optional filename to save the downloaded archive under `dependencies/tmp`
- `checksum` - optional sha256 checksum to verify the archive - `checksum` - optional sha256 checksum to verify the archive
- `extract_to` - directory under `dependencies/` where files should be extracted or cloned - `extract_to` - directory under `dependencies/` where files should be extracted or cloned
- `build_cmds` - optional semicolon-separated shell commands to build/install the dependency
Notes: Notes:
- Only `name` is required. If `url` is omitted Lana will skip any download/clone and extraction steps — this is useful for dependencies that are generated locally or that only require running project-local commands.
- If `url` points to a git repository (ends with `.git`), `lana setup` will perform a shallow clone into `dependencies/<extract_to>`. - If `url` points to a git repository (ends with `.git`), `lana setup` will perform a shallow clone into `dependencies/<extract_to>`.
- For archive URLs `lana setup` will try `curl` then `wget` to download, will verify checksum if provided, and will extract common archive types (`.tar.gz`, `.tar.xz`, `.zip`). - For archive URLs `lana setup` will try `curl` then `wget` to download, will verify checksum if provided, and will extract common archive types (`.tar.gz`, `.tar.xz`, `.zip`).
- When `build_cmds` are present they are executed either inside `dependencies/<extract_to>` (if `extract_to` is set or a clone/extract was performed) or in the project root (if no extract directory is available).
- The current implementation performs a best-effort download/extract and prints warnings/errors; it is intentionally simple and can be extended or replaced by a more robust script if needed. - The current implementation performs a best-effort download/extract and prints warnings/errors; it is intentionally simple and can be extended or replaced by a more robust script if needed.
Example (only `name` + `build_cmds`):
```ini
[dependencies]
name = generate_headers
build_cmds = tools/gen_headers.sh; cp -r generated/include ../../include/
```
In this example `lana setup` will not try to download anything — it will run the `build_cmds` from the project root, allowing you to run arbitrary local build or generation steps.
## Configuration ## Configuration
`config.ini` handles **global** settings (overridden by directives for per-file needs). Edit it in your project root. `config.ini` handles **global** settings (overridden by directives for per-file needs). Edit it in your project root.

112
deps/deps.v vendored
View File

@ -93,42 +93,43 @@ pub fn fetch_dependencies(build_config config.BuildConfig) ! {
println('Processing dependency: ${dep.name}') println('Processing dependency: ${dep.name}')
println(' parsed: url="${dep.url}", archive="${dep.archive}", extract_to="${dep.extract_to}"') println(' parsed: url="${dep.url}", archive="${dep.archive}", extract_to="${dep.extract_to}"')
if dep.url.trim_space() == '' { // Allow dependencies with only a name. If no URL is provided, skip
return error('Dependency ${dep.name} has empty url in config') // download/extract/clone steps and only run any provided build_cmds.
} url_trim := dep.url.trim_space()
// Decide if URL is a git repo or an archive // Decide if URL is a git repo or an archive (only if URL present)
is_git := dep.url.ends_with('.git') || dep.url.starts_with('git://') is_git := url_trim != '' && (url_trim.ends_with('.git') || url_trim.starts_with('git://'))
extract_to := if dep.extract_to != '' { os.join_path(deps_dir, dep.extract_to) } else { os.join_path(deps_dir, dep.name) } extract_to := if dep.extract_to != '' { os.join_path(deps_dir, dep.extract_to) } else { os.join_path(deps_dir, dep.name) }
if url_trim == '' {
println('No url provided for ${dep.name}, skipping download/extract; will only run build_cmds if present')
} else {
if is_git { if is_git {
// Clone repository // Clone repository if needed
if os.is_dir(extract_to) { if os.is_dir(extract_to) {
println('Dependency already cloned at ${extract_to}, skipping') println('Dependency already cloned at ${extract_to}, skipping clone')
continue } else {
} cmd := 'git clone --depth 1 ${url_trim} ${extract_to}'
cmd := 'git clone --depth 1 ${dep.url} ${extract_to}'
println('Running: ${cmd}') println('Running: ${cmd}')
res := os.execute(cmd) code := os.system(cmd)
if res.exit_code != 0 { if code != 0 {
return error('Failed to clone ${dep.url}: ${res.output}') return error('Failed to clone ${url_trim}: exit ${code}')
} }
continue
} }
} else {
// Archive download path // Archive download path
archive_name := if dep.archive != '' { dep.archive } else { os.file_name(dep.url) } archive_name := if dep.archive != '' { dep.archive } else { os.file_name(url_trim) }
archive_path := os.join_path(tmp_dir, archive_name) archive_path := os.join_path(tmp_dir, archive_name)
if !os.is_file(archive_path) { if !os.is_file(archive_path) {
println('Downloading ${dep.url} -> ${archive_path}') println('Downloading ${url_trim} -> ${archive_path}')
// Prefer curl, fall back to wget // Prefer curl, fall back to wget
mut res := os.execute('curl -L -o ${archive_path} ${dep.url}') mut code := os.system('curl -L -o ${archive_path} ${url_trim}')
if res.exit_code != 0 { if code != 0 {
res = os.execute('wget -O ${archive_path} ${dep.url}') code = os.system('wget -O ${archive_path} ${url_trim}')
if res.exit_code != 0 { if code != 0 {
return error('Failed to download ${dep.url}: ${res.output}') return error('Failed to download ${url_trim}: curl/wget exit ${code}')
} }
} }
} else { } else {
@ -152,8 +153,7 @@ pub fn fetch_dependencies(build_config config.BuildConfig) ! {
// Extract archive // Extract archive
if os.is_dir(extract_to) { if os.is_dir(extract_to) {
println('Already extracted to ${extract_to}, skipping') println('Already extracted to ${extract_to}, skipping')
continue } else {
}
os.mkdir_all(extract_to) or { return error('Failed to create ${extract_to}: ${err}') } os.mkdir_all(extract_to) or { return error('Failed to create ${extract_to}: ${err}') }
// Basic extraction handling by extension // Basic extraction handling by extension
@ -161,34 +161,44 @@ pub fn fetch_dependencies(build_config config.BuildConfig) ! {
if lower.ends_with('.tar.gz') || lower.ends_with('.tgz') || lower.ends_with('.tar.xz') || lower.ends_with('.tar') { if lower.ends_with('.tar.gz') || lower.ends_with('.tgz') || lower.ends_with('.tar.xz') || lower.ends_with('.tar') {
cmd := 'tar -xf ${archive_path} -C ${deps_dir}' cmd := 'tar -xf ${archive_path} -C ${deps_dir}'
println('Extracting with: ${cmd}') println('Extracting with: ${cmd}')
res := os.execute(cmd) code := os.system(cmd)
if res.exit_code != 0 { if code != 0 {
return error('Failed to extract ${archive_path}: ${res.output}') return error('Failed to extract ${archive_path}: exit ${code}')
} }
// If the archive created a top-level dir, move/rename it to extract_to if needed // If the archive created a top-level dir, caller should set extract_to to match archive content.
// We won't attempt to be clever here; caller should set extract_to to match archive content.
} else if lower.ends_with('.zip') { } else if lower.ends_with('.zip') {
cmd := 'unzip -q ${archive_path} -d ${extract_to}' cmd := 'unzip -q ${archive_path} -d ${extract_to}'
println('Extracting zip with: ${cmd}') println('Extracting zip with: ${cmd}')
res := os.execute(cmd) code := os.system(cmd)
if res.exit_code != 0 { if code != 0 {
return error('Failed to unzip ${archive_path}: ${res.output}') return error('Failed to unzip ${archive_path}: exit ${code}')
} }
} else { } else {
println('Unknown archive format for ${archive_path}, skipping extraction') println('Unknown archive format for ${archive_path}, skipping extraction')
} }
}
}
}
// Run build commands if provided, otherwise run package-specific defaults // Run build commands if provided, otherwise run package-specific defaults
if dep.build_cmds.len > 0 { if dep.build_cmds.len > 0 {
// Choose where to run build commands:
// - If extract_to was provided, run inside deps/<extract_to> (create it if missing)
// - Otherwise run from project root (os.getwd())
mut run_dir := os.getwd()
if dep.extract_to != '' {
run_dir = os.join_path(deps_dir, dep.extract_to)
os.mkdir_all(run_dir) or { println('Warning: Failed to create build dir ${run_dir}: ${err}') }
}
for cmd_line in dep.build_cmds { for cmd_line in dep.build_cmds {
println('Running build command for ${dep.name}: ${cmd_line}') println('Running build command for ${dep.name}: ${cmd_line}')
// run in extract_to
old_cwd := os.getwd() old_cwd := os.getwd()
os.chdir(extract_to) or { return error('Failed to chdir: ${err}') } os.chdir(run_dir) or { return error('Failed to chdir: ${err}') }
res := os.execute(cmd_line) code := os.system(cmd_line)
os.chdir(old_cwd) or { } os.chdir(old_cwd) or { }
if res.exit_code != 0 { if code != 0 {
return error('Build command failed for ${dep.name}: ${res.output}') return error('Build command failed for ${dep.name}: exit ${code}')
} }
} }
} else { } else {
@ -198,11 +208,11 @@ pub fn fetch_dependencies(build_config config.BuildConfig) ! {
println('Building zlib...') println('Building zlib...')
old_cwd := os.getwd() old_cwd := os.getwd()
os.chdir(extract_to) or { return error('Failed to chdir: ${err}') } os.chdir(extract_to) or { return error('Failed to chdir: ${err}') }
mut res := os.execute('./configure') mut code := os.system('./configure')
if res.exit_code != 0 { os.chdir(old_cwd) or {} ; return error('zlib configure failed: ${res.output}') } if code != 0 { os.chdir(old_cwd) or {} ; return error('zlib configure failed: exit ${code}') }
res = os.execute('make') code = os.system('make')
os.chdir(old_cwd) or {} os.chdir(old_cwd) or {}
if res.exit_code != 0 { return error('zlib make failed: ${res.output}') } if code != 0 { return error('zlib make failed: exit ${code}') }
} }
'sockpp' { 'sockpp' {
println('Building sockpp...') println('Building sockpp...')
@ -211,27 +221,27 @@ pub fn fetch_dependencies(build_config config.BuildConfig) ! {
os.mkdir_all(build_dir) or { return error('Failed to create build dir: ${err}') } os.mkdir_all(build_dir) or { return error('Failed to create build dir: ${err}') }
old_cwd := os.getwd() old_cwd := os.getwd()
os.chdir(extract_to) or { return error('Failed to chdir: ${err}') } os.chdir(extract_to) or { return error('Failed to chdir: ${err}') }
mut res := os.execute('cmake -Bbuild .') mut code := os.system('cmake -Bbuild .')
if res.exit_code != 0 { os.chdir(old_cwd) or {} ; return error('sockpp cmake failed: ${res.output}') } if code != 0 { os.chdir(old_cwd) or {} ; return error('sockpp cmake failed: exit ${code}') }
res = os.execute('cmake --build build') code = os.system('cmake --build build')
os.chdir(old_cwd) or {} os.chdir(old_cwd) or {}
if res.exit_code != 0 { return error('sockpp build failed: ${res.output}') } if code != 0 { return error('sockpp build failed: exit ${code}') }
} }
'shaderc' { 'shaderc' {
println('Building shaderc (invoke update script + ninja)') println('Building shaderc (invoke update script + ninja)')
old_cwd := os.getwd() old_cwd := os.getwd()
os.chdir(extract_to) or { return error('Failed to chdir: ${err}') } os.chdir(extract_to) or { return error('Failed to chdir: ${err}') }
mut res := os.execute('./update_shaderc_sources.py') mut code := os.system('./update_shaderc_sources.py')
if res.exit_code != 0 { os.chdir(old_cwd) or {} ; return error('shaderc update failed: ${res.output}') } if code != 0 { os.chdir(old_cwd) or {} ; return error('shaderc update failed: exit ${code}') }
// create build dir // create build dir
build_dir := 'build-$(date +%s)' build_dir := 'build-$(date +%s)'
os.mkdir_all(build_dir) or { os.chdir(old_cwd) or {} ; return error('Failed to create shaderc build dir') } os.mkdir_all(build_dir) or { os.chdir(old_cwd) or {} ; return error('Failed to create shaderc build dir') }
os.chdir(build_dir) or { os.chdir(old_cwd) or {} ; return error('Failed to chdir to shaderc build dir') } os.chdir(build_dir) or { os.chdir(old_cwd) or {} ; return error('Failed to chdir to shaderc build dir') }
res = os.execute('cmake -GNinja -DCMAKE_BUILD_TYPE=Release ../src/') code = os.system('cmake -GNinja -DCMAKE_BUILD_TYPE=Release ../src/')
if res.exit_code != 0 { os.chdir(old_cwd) or {} ; return error('shaderc cmake failed: ${res.output}') } if code != 0 { os.chdir(old_cwd) or {} ; return error('shaderc cmake failed: exit ${code}') }
res = os.execute('ninja') code = os.system('ninja')
os.chdir(old_cwd) or {} os.chdir(old_cwd) or {}
if res.exit_code != 0 { return error('shaderc ninja failed: ${res.output}') } if code != 0 { return error('shaderc ninja failed: exit ${code}') }
// attempt to copy glslc to dependencies/shaderc/bin (best-effort) // attempt to copy glslc to dependencies/shaderc/bin (best-effort)
glslc_path := os.join_path(extract_to, build_dir, 'glslc', 'glslc') glslc_path := os.join_path(extract_to, build_dir, 'glslc', 'glslc')
out_dir := os.join_path(build_config.dependencies_dir, dep.extract_to) out_dir := os.join_path(build_config.dependencies_dir, dep.extract_to)