support LD_PRELOAD, and write version.h
6 files changed +206 −29
- + tests/11_ld_preload/Makefile
- + version.h.in
- • CHANGELOG.md
- • CMakeLists.txt
- • doc/libtree.1
- • libtree.c
| 1 | diff --git a/CHANGELOG.md b/CHANGELOG.md |
| 2 | index dace919..6efed49 100644 |
| 3 | --- a/CHANGELOG.md |
| 4 | +++ b/CHANGELOG.md |
| 5 | @@ -1,3 +1,9 @@ |
| 6 | +# v3.2.0 |
| 7 | +- Add support for `LD_PRELOAD`: libraries listed in this environment variable |
| 8 | + are shown as dependencies of each top-level file, marked `[LD_PRELOAD]`. |
| 9 | +- CMake build system |
| 10 | +- Static executable |
| 11 | + |
| 12 | # v3.1.1 |
| 13 | - Build system portability fixes |
| 14 | - Fix make check exit code |
| 15 | diff --git a/CMakeLists.txt b/CMakeLists.txt |
| 16 | index acf8bdc..4964c4f 100644 |
| 17 | --- a/CMakeLists.txt |
| 18 | +++ b/CMakeLists.txt |
| 19 | @@ -1,14 +1,18 @@ |
| 20 | cmake_minimum_required(VERSION 3.10) |
| 21 | |
| 22 | -project(libtree C) |
| 23 | +project(libtree VERSION 3.2.0 LANGUAGES C) |
| 24 | |
| 25 | set(CMAKE_C_STANDARD 99) |
| 26 | set(CMAKE_C_STANDARD_REQUIRED ON) |
| 27 | |
| 28 | include(GNUInstallDirs) |
| 29 | |
| 30 | +configure_file(version.h.in version.h @ONLY) |
| 31 | + |
| 32 | add_executable(libtree libtree.c) |
| 33 | |
| 34 | +target_include_directories(libtree PRIVATE ${CMAKE_CURRENT_BINARY_DIR}) |
| 35 | + |
| 36 | target_compile_definitions(libtree PRIVATE _FILE_OFFSET_BITS=64) |
| 37 | |
| 38 | target_compile_options(libtree PRIVATE |
| 39 | diff --git a/doc/libtree.1 b/doc/libtree.1 |
| 40 | index ff87b85..ae9ca07 100644 |
| 41 | --- a/doc/libtree.1 |
| 42 | +++ b/doc/libtree.1 |
| 43 | @@ -43,6 +43,14 @@ All arguments after '--' are interpreted as paths, not flags. |
| 44 | .SH ENVIRONMENT |
| 45 | .B LD_LIBRARY_PATH |
| 46 | can be used to provide additional search paths. |
| 47 | +.PP |
| 48 | +.B LD_PRELOAD |
| 49 | +can be used to specify additional libraries (separated by spaces and/or |
| 50 | +colons) to be loaded before all others. These are shown as direct |
| 51 | +dependencies of each top-level file, marked |
| 52 | +.BR [LD_PRELOAD] , |
| 53 | +and are located using the same search rules as that file's regular |
| 54 | +dependencies. |
| 55 | .SH AUTHOR |
| 56 | Harmen Stoppels <me@harmenstoppels.nl> |
| 57 | .SH "SEE ALSO" |
| 58 | diff --git a/libtree.c b/libtree.c |
| 59 | index aee4187..3753c6a 100644 |
| 60 | --- a/libtree.c |
| 61 | +++ b/libtree.c |
| 62 | @@ -10,7 +10,7 @@ |
| 63 | #include <sys/utsname.h> |
| 64 | #include <unistd.h> |
| 65 | |
| 66 | -#define VERSION "3.2.0-dev" |
| 67 | +#include "version.h" |
| 68 | |
| 69 | #define ET_EXEC 2 |
| 70 | #define ET_DYN 3 |
| 71 | @@ -176,9 +176,20 @@ typedef enum { |
| 72 | LD_LIBRARY_PATH, |
| 73 | RUNPATH, |
| 74 | LD_SO_CONF, |
| 75 | - DEFAULT |
| 76 | + DEFAULT, |
| 77 | + LD_PRELOAD |
| 78 | } how_t; |
| 79 | |
| 80 | +/** |
| 81 | + * small_vec_u64 is an array that lives on the stack until it grows to the heap |
| 82 | + */ |
| 83 | +struct small_vec_u64_t { |
| 84 | + uint64_t buf[SMALL_VEC_SIZE]; |
| 85 | + uint64_t *p; |
| 86 | + size_t n; |
| 87 | + size_t capacity; |
| 88 | +}; |
| 89 | + |
| 90 | struct found_t { |
| 91 | how_t how; |
| 92 | // only set when found by in the rpath NOT of the direct parent. so, when |
| 93 | @@ -233,6 +244,10 @@ struct libtree_state_t { |
| 94 | size_t default_paths_offset; |
| 95 | size_t ld_so_conf_offset; |
| 96 | |
| 97 | + // Offsets into the string table of each LD_PRELOAD entry, stored as |
| 98 | + // separate null-terminated strings. |
| 99 | + struct small_vec_u64_t preload_offsets; |
| 100 | + |
| 101 | // This is so we know we have to print a | or white space |
| 102 | // in the tree |
| 103 | char found_all_needed[MAX_RECURSION_DEPTH]; |
| 104 | @@ -240,16 +255,6 @@ struct libtree_state_t { |
| 105 | |
| 106 | // Keep track of the files we've see |
| 107 | |
| 108 | -/** |
| 109 | - * small_vec_u64 is an array that lives on the stack until it grows to the heap |
| 110 | - */ |
| 111 | -struct small_vec_u64_t { |
| 112 | - uint64_t buf[SMALL_VEC_SIZE]; |
| 113 | - uint64_t *p; |
| 114 | - size_t n; |
| 115 | - size_t capacity; |
| 116 | -}; |
| 117 | - |
| 118 | static inline void utoa(char *str, size_t v) { |
| 119 | char *p = str; |
| 120 | do { |
| 121 | @@ -418,7 +423,8 @@ static void apply_exclude_list(size_t *needed_not_found, |
| 122 | static int check_absolute_paths(size_t *needed_not_found, |
| 123 | struct small_vec_u64_t *needed_buf_offsets, |
| 124 | size_t depth, struct libtree_state_t *s, |
| 125 | - struct compat_t compat) { |
| 126 | + struct compat_t compat, struct found_t reason, |
| 127 | + size_t extra_after) { |
| 128 | int exit_code = 0; |
| 129 | // First go over absolute paths in needed libs. |
| 130 | for (size_t i = 0; i < *needed_not_found;) { |
| 131 | @@ -441,7 +447,7 @@ static int check_absolute_paths(size_t *needed_not_found, |
| 132 | // Include \0 |
| 133 | memcpy(path, st->arr + needed_buf_offsets->p[i], len + 1); |
| 134 | |
| 135 | - s->found_all_needed[depth] = *needed_not_found <= 1; |
| 136 | + s->found_all_needed[depth] = *needed_not_found <= 1 && extra_after == 0; |
| 137 | char *err = NULL; |
| 138 | |
| 139 | // If it is not an absolute path, we bail, cause it then starts to |
| 140 | @@ -451,8 +457,7 @@ static int check_absolute_paths(size_t *needed_not_found, |
| 141 | err = " is not absolute"; |
| 142 | exit_code = ERR_DEPENDENCY_NOT_FOUND; |
| 143 | } else { |
| 144 | - int code = recurse(path, depth + 1, s, compat, |
| 145 | - (struct found_t){.how = DIRECT}); |
| 146 | + int code = recurse(path, depth + 1, s, compat, reason); |
| 147 | if (code == ERR_DEPENDENCY_NOT_FOUND) |
| 148 | exit_code = ERR_DEPENDENCY_NOT_FOUND; |
| 149 | |
| 150 | @@ -485,7 +490,7 @@ static int check_search_paths(struct found_t reason, size_t offset, |
| 151 | size_t *needed_not_found, |
| 152 | struct small_vec_u64_t *needed_buf_offsets, |
| 153 | size_t depth, struct libtree_state_t *s, |
| 154 | - struct compat_t compat) { |
| 155 | + struct compat_t compat, size_t extra_after) { |
| 156 | int exit_code = 0; |
| 157 | char path[MAX_PATH_LENGTH]; |
| 158 | char *path_end = path + MAX_PATH_LENGTH; |
| 159 | @@ -529,7 +534,8 @@ static int check_search_paths(struct found_t reason, size_t offset, |
| 160 | // Otherwise append. |
| 161 | memcpy(search_path_end, st->arr + needed_buf_offsets->p[i], |
| 162 | soname_len + 1); |
| 163 | - s->found_all_needed[depth] = *needed_not_found <= 1; |
| 164 | + s->found_all_needed[depth] = |
| 165 | + *needed_not_found <= 1 && extra_after == 0; |
| 166 | |
| 167 | // And try to locate the lib. |
| 168 | int code = recurse(path, depth + 1, s, compat, reason); |
| 169 | @@ -712,6 +718,9 @@ static void print_line(size_t depth, char *name, char *color_bold, |
| 170 | case LD_LIBRARY_PATH: |
| 171 | fputs("[LD_LIBRARY_PATH]", stdout); |
| 172 | break; |
| 173 | + case LD_PRELOAD: |
| 174 | + fputs("[LD_PRELOAD]", stdout); |
| 175 | + break; |
| 176 | case RUNPATH: |
| 177 | fputs("[runpath]", stdout); |
| 178 | break; |
| 179 | @@ -740,9 +749,10 @@ static void print_line(size_t depth, char *name, char *color_bold, |
| 180 | static void print_error(size_t depth, size_t needed_not_found, |
| 181 | struct small_vec_u64_t *needed_buf_offsets, |
| 182 | char *runpath, struct libtree_state_t *s, |
| 183 | - int no_def_lib) { |
| 184 | + int no_def_lib, size_t extra_after) { |
| 185 | for (size_t i = 0; i < needed_not_found; ++i) { |
| 186 | - s->found_all_needed[depth] = i + 1 >= needed_not_found; |
| 187 | + s->found_all_needed[depth] = |
| 188 | + i + 1 >= needed_not_found && extra_after == 0; |
| 189 | tree_preamble(s, depth + 1); |
| 190 | if (s->color) |
| 191 | fputs(BOLD_RED, stdout); |
| 192 | @@ -1318,9 +1328,79 @@ static int recurse(char *current_file, size_t depth, struct libtree_state_t *s, |
| 193 | if (needed_not_found && s->verbosity == 0) |
| 194 | apply_exclude_list(&needed_not_found, &needed_buf_offsets, s); |
| 195 | |
| 196 | + // LD_PRELOAD only applies to the top-level object: those libraries are |
| 197 | + // loaded by the dynamic linker before anything else, using the same |
| 198 | + // search rules as the regular dependencies of the top-level object. |
| 199 | + struct small_vec_u64_t preload_buf_offsets; |
| 200 | + small_vec_u64_init(&preload_buf_offsets); |
| 201 | + size_t preload_not_found = 0; |
| 202 | + |
| 203 | + if (depth == 0 && s->preload_offsets.n > 0) { |
| 204 | + for (size_t i = 0; i < s->preload_offsets.n; ++i) |
| 205 | + small_vec_u64_append(&preload_buf_offsets, s->preload_offsets.p[i]); |
| 206 | + preload_not_found = preload_buf_offsets.n; |
| 207 | + |
| 208 | + if (preload_not_found) |
| 209 | + exit_code |= check_absolute_paths( |
| 210 | + &preload_not_found, &preload_buf_offsets, depth, s, curr_type, |
| 211 | + (struct found_t){.how = LD_PRELOAD}, needed_not_found); |
| 212 | + |
| 213 | + if (preload_not_found && runpath == MAX_OFFSET_T) { |
| 214 | + for (int j = depth; j >= 0 && preload_not_found; --j) { |
| 215 | + if (s->rpath_offsets[j] == SIZE_MAX) |
| 216 | + continue; |
| 217 | + |
| 218 | + exit_code |= check_search_paths( |
| 219 | + (struct found_t){.how = LD_PRELOAD}, s->rpath_offsets[j], |
| 220 | + &preload_not_found, &preload_buf_offsets, depth, s, |
| 221 | + curr_type, needed_not_found); |
| 222 | + } |
| 223 | + } |
| 224 | + |
| 225 | + if (preload_not_found && s->ld_library_path_offset != SIZE_MAX) { |
| 226 | + exit_code |= check_search_paths( |
| 227 | + (struct found_t){.how = LD_PRELOAD}, s->ld_library_path_offset, |
| 228 | + &preload_not_found, &preload_buf_offsets, depth, s, curr_type, |
| 229 | + needed_not_found); |
| 230 | + } |
| 231 | + |
| 232 | + if (preload_not_found && runpath != MAX_OFFSET_T) { |
| 233 | + exit_code |= check_search_paths( |
| 234 | + (struct found_t){.how = LD_PRELOAD}, runpath_buf_offset, |
| 235 | + &preload_not_found, &preload_buf_offsets, depth, s, curr_type, |
| 236 | + needed_not_found); |
| 237 | + } |
| 238 | + |
| 239 | + if (preload_not_found && !no_def_lib) { |
| 240 | + exit_code |= check_search_paths( |
| 241 | + (struct found_t){.how = LD_PRELOAD}, s->ld_so_conf_offset, |
| 242 | + &preload_not_found, &preload_buf_offsets, depth, s, curr_type, |
| 243 | + needed_not_found); |
| 244 | + } |
| 245 | + |
| 246 | + if (preload_not_found && !no_def_lib) { |
| 247 | + exit_code |= check_search_paths( |
| 248 | + (struct found_t){.how = LD_PRELOAD}, s->default_paths_offset, |
| 249 | + &preload_not_found, &preload_buf_offsets, depth, s, curr_type, |
| 250 | + needed_not_found); |
| 251 | + } |
| 252 | + |
| 253 | + if (preload_not_found) { |
| 254 | + print_error(depth, preload_not_found, &preload_buf_offsets, |
| 255 | + runpath == MAX_OFFSET_T |
| 256 | + ? NULL |
| 257 | + : s->string_table.arr + runpath_buf_offset, |
| 258 | + s, no_def_lib, needed_not_found); |
| 259 | + exit_code |= ERR_DEPENDENCY_NOT_FOUND; |
| 260 | + } |
| 261 | + } |
| 262 | + |
| 263 | + small_vec_u64_free(&preload_buf_offsets); |
| 264 | + |
| 265 | if (needed_not_found) |
| 266 | - exit_code |= check_absolute_paths( |
| 267 | - &needed_not_found, &needed_buf_offsets, depth, s, curr_type); |
| 268 | + exit_code |= check_absolute_paths(&needed_not_found, &needed_buf_offsets, |
| 269 | + depth, s, curr_type, |
| 270 | + (struct found_t){.how = DIRECT}, 0); |
| 271 | |
| 272 | // Consider rpaths only when runpath is empty |
| 273 | if (runpath == MAX_OFFSET_T) { |
| 274 | @@ -1332,7 +1412,7 @@ static int recurse(char *current_file, size_t depth, struct libtree_state_t *s, |
| 275 | |
| 276 | exit_code |= check_search_paths( |
| 277 | (struct found_t){.how = RPATH, .depth = j}, s->rpath_offsets[j], |
| 278 | - &needed_not_found, &needed_buf_offsets, depth, s, curr_type); |
| 279 | + &needed_not_found, &needed_buf_offsets, depth, s, curr_type, 0); |
| 280 | } |
| 281 | } |
| 282 | |
| 283 | @@ -1340,28 +1420,28 @@ static int recurse(char *current_file, size_t depth, struct libtree_state_t *s, |
| 284 | if (needed_not_found && s->ld_library_path_offset != SIZE_MAX) { |
| 285 | exit_code |= check_search_paths( |
| 286 | (struct found_t){.how = LD_LIBRARY_PATH}, s->ld_library_path_offset, |
| 287 | - &needed_not_found, &needed_buf_offsets, depth, s, curr_type); |
| 288 | + &needed_not_found, &needed_buf_offsets, depth, s, curr_type, 0); |
| 289 | } |
| 290 | |
| 291 | // Then consider runpaths |
| 292 | if (needed_not_found && runpath != MAX_OFFSET_T) { |
| 293 | exit_code |= check_search_paths( |
| 294 | (struct found_t){.how = RUNPATH}, runpath_buf_offset, |
| 295 | - &needed_not_found, &needed_buf_offsets, depth, s, curr_type); |
| 296 | + &needed_not_found, &needed_buf_offsets, depth, s, curr_type, 0); |
| 297 | } |
| 298 | |
| 299 | // Check ld.so.conf paths |
| 300 | if (needed_not_found && !no_def_lib) { |
| 301 | exit_code |= check_search_paths( |
| 302 | (struct found_t){.how = LD_SO_CONF}, s->ld_so_conf_offset, |
| 303 | - &needed_not_found, &needed_buf_offsets, depth, s, curr_type); |
| 304 | + &needed_not_found, &needed_buf_offsets, depth, s, curr_type, 0); |
| 305 | } |
| 306 | |
| 307 | // Then consider standard paths |
| 308 | if (needed_not_found && !no_def_lib) { |
| 309 | exit_code |= check_search_paths( |
| 310 | (struct found_t){.how = DEFAULT}, s->default_paths_offset, |
| 311 | - &needed_not_found, &needed_buf_offsets, depth, s, curr_type); |
| 312 | + &needed_not_found, &needed_buf_offsets, depth, s, curr_type, 0); |
| 313 | } |
| 314 | |
| 315 | // Finally summarize those that could not be found. |
| 316 | @@ -1370,7 +1450,7 @@ static int recurse(char *current_file, size_t depth, struct libtree_state_t *s, |
| 317 | runpath == MAX_OFFSET_T |
| 318 | ? NULL |
| 319 | : s->string_table.arr + runpath_buf_offset, |
| 320 | - s, no_def_lib); |
| 321 | + s, no_def_lib, 0); |
| 322 | s->string_table.n = old_buf_size; |
| 323 | small_vec_u64_free(&needed_buf_offsets); |
| 324 | small_vec_u64_free(&needed); |
| 325 | @@ -1529,6 +1609,38 @@ static void parse_ld_library_path(struct libtree_state_t *s) { |
| 326 | *search++ = ':'; |
| 327 | } |
| 328 | |
| 329 | +static void parse_ld_preload(struct libtree_state_t *s) { |
| 330 | + small_vec_u64_init(&s->preload_offsets); |
| 331 | + |
| 332 | + char *val = getenv("LD_PRELOAD"); |
| 333 | + if (val == NULL) |
| 334 | + return; |
| 335 | + |
| 336 | + // LD_PRELOAD entries are separated by spaces, tabs and/or colons. Store |
| 337 | + // each entry as its own null-terminated string in the string table. |
| 338 | + char *p = val; |
| 339 | + while (1) { |
| 340 | + while (*p == ':' || isspace((unsigned char)*p)) |
| 341 | + ++p; |
| 342 | + |
| 343 | + if (*p == '\0') |
| 344 | + break; |
| 345 | + |
| 346 | + char *start = p; |
| 347 | + while (*p != '\0' && *p != ':' && !isspace((unsigned char)*p)) |
| 348 | + ++p; |
| 349 | + |
| 350 | + size_t len = p - start; |
| 351 | + size_t offset = s->string_table.n; |
| 352 | + string_table_maybe_grow(&s->string_table, len + 1); |
| 353 | + memcpy(s->string_table.arr + offset, start, len); |
| 354 | + s->string_table.arr[offset + len] = '\0'; |
| 355 | + s->string_table.n += len + 1; |
| 356 | + |
| 357 | + small_vec_u64_append(&s->preload_offsets, offset); |
| 358 | + } |
| 359 | +} |
| 360 | + |
| 361 | static void set_default_paths(struct libtree_state_t *s) { |
| 362 | s->default_paths_offset = s->string_table.n; |
| 363 | // TODO: how to retrieve this list properly at runtime? |
| 364 | @@ -1548,6 +1660,7 @@ static void libtree_state_init(struct libtree_state_t *s) { |
| 365 | static void libtree_state_free(struct libtree_state_t *s) { |
| 366 | free(s->string_table.arr); |
| 367 | free(s->visited.arr); |
| 368 | + small_vec_u64_free(&s->preload_offsets); |
| 369 | } |
| 370 | |
| 371 | static int print_tree(int pathc, char **pathv, struct libtree_state_t *s) { |
| 372 | @@ -1556,6 +1669,7 @@ static int print_tree(int pathc, char **pathv, struct libtree_state_t *s) { |
| 373 | |
| 374 | parse_ld_so_conf(s); |
| 375 | parse_ld_library_path(s); |
| 376 | + parse_ld_preload(s); |
| 377 | set_default_paths(s); |
| 378 | |
| 379 | int exit_code = 0; |
| 380 | @@ -1790,6 +1904,10 @@ int main(int argc, char **argv) { |
| 381 | fputs("]\n" |
| 382 | " --max-depth <n> Limit library traversal to at most n levels of depth\n" |
| 383 | "\n" |
| 384 | + "Environment variables:\n" |
| 385 | + " LD_LIBRARY_PATH Additional search paths\n" |
| 386 | + " LD_PRELOAD Additional libraries loaded before all others\n" |
| 387 | + "\n" |
| 388 | "* For brevity, the following libraries are not shown by default:\n" |
| 389 | " ", |
| 390 | stdout); |
| 391 | diff --git a/tests/11_ld_preload/Makefile b/tests/11_ld_preload/Makefile |
| 392 | new file mode 100644 |
| 393 | index 0000000..884ba7f |
| 394 | --- /dev/null |
| 395 | +++ b/tests/11_ld_preload/Makefile |
| 396 | @@ -0,0 +1,40 @@ |
| 397 | +LIBTREE ?= ../../libtree |
| 398 | + |
| 399 | +# Basic test for LD_PRELOAD support: preloaded libraries should show up as |
| 400 | +# direct dependencies of the top-level file, marked [LD_PRELOAD], and should |
| 401 | +# be located using the same search rules (absolute path, LD_LIBRARY_PATH, |
| 402 | +# rpath, ...) as the top-level file's regular dependencies. |
| 403 | + |
| 404 | +.PHONY: clean |
| 405 | + |
| 406 | +LD_LIBRARY_PATH= |
| 407 | + |
| 408 | +all: check |
| 409 | + |
| 410 | +libb.so: |
| 411 | + echo 'int b(void){return 1;}' | $(CC) -shared -Wl,-soname,$@ -Wl,--no-as-needed -o $@ -nostdlib -Wno-implicit-function-declaration -x c - |
| 412 | + |
| 413 | +libpre1.so: |
| 414 | + echo 'int p1(void){return 2;}' | $(CC) -shared -Wl,-soname,$@ -Wl,--no-as-needed -o $@ -nostdlib -Wno-implicit-function-declaration -x c - |
| 415 | + |
| 416 | +libpre2.so: |
| 417 | + echo 'int p2(void){return 3;}' | $(CC) -shared -Wl,-soname,$@ -Wl,--no-as-needed -o $@ -nostdlib -Wno-implicit-function-declaration -x c - |
| 418 | + |
| 419 | +exe: libb.so |
| 420 | + echo 'int b(void); int _start(void){return b();}' | $(CC) -Wl,--no-as-needed -Wl,--disable-new-dtags "-Wl,-rpath,$(CURDIR)" libb.so -o $@ -Wno-implicit-function-declaration -nostdlib -x c - |
| 421 | + |
| 422 | +check: exe libpre1.so libpre2.so |
| 423 | + # Found via absolute path |
| 424 | + LD_PRELOAD="$(CURDIR)/libpre1.so" $(LIBTREE) exe | grep -q 'libpre1.so \[LD_PRELOAD\]' |
| 425 | + # Found via LD_LIBRARY_PATH, multiple entries (space and colon separated) |
| 426 | + LD_PRELOAD="libpre1.so libpre2.so" LD_LIBRARY_PATH="$(CURDIR)" $(LIBTREE) exe | grep -q 'libpre1.so \[LD_PRELOAD\]' |
| 427 | + LD_PRELOAD="libpre1.so:libpre2.so" LD_LIBRARY_PATH="$(CURDIR)" $(LIBTREE) exe | grep -q 'libpre2.so \[LD_PRELOAD\]' |
| 428 | + # Without LD_PRELOAD, no [LD_PRELOAD] entries are shown |
| 429 | + ! $(LIBTREE) exe | grep -q '\[LD_PRELOAD\]' |
| 430 | + # A library that cannot be found is reported as such |
| 431 | + LD_PRELOAD="libdoesnotexist.so" $(LIBTREE) exe | grep -q 'libdoesnotexist.so not found' |
| 432 | + |
| 433 | +clean: |
| 434 | + rm -f *.so exe* |
| 435 | + |
| 436 | +CURDIR ?= $(.CURDIR) |
| 437 | diff --git a/version.h.in b/version.h.in |
| 438 | new file mode 100644 |
| 439 | index 0000000..32f49cf |
| 440 | --- /dev/null |
| 441 | +++ b/version.h.in |
| 442 | @@ -0,0 +1 @@ |
| 443 | +#define VERSION "@PROJECT_VERSION@" |