1 /**************************************************************************
3 * Copyright (c) 2004-19 Simon Peter
7 * Permission is hereby granted, free of charge, to any person obtaining a copy
8 * of this software and associated documentation files (the "Software"), to deal
9 * in the Software without restriction, including without limitation the rights
10 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11 * copies of the Software, and to permit persons to whom the Software is
12 * furnished to do so, subject to the following conditions:
14 * The above copyright notice and this permission notice shall be included in
15 * all copies or substantial portions of the Software.
17 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
25 **************************************************************************/
27 #ident "AppImage by Simon Peter, http://appimage.org/"
30 #define RELEASE_NAME "continuous build"
34 #include <glib/gstdio.h>
41 #include "squashfuse.h"
43 #include <sys/types.h>
56 #include "appimage/appimage.h"
59 #define HAVE_BINARY_RUNTIME
60 extern char runtime
[];
61 extern unsigned int runtime_len
;
71 static gchar
const APPIMAGEIGNORE
[] = ".appimageignore";
72 static char _exclude_file_desc
[256];
74 static gboolean list
= FALSE
;
75 static gboolean verbose
= FALSE
;
76 static gboolean showVersionOnly
= FALSE
;
77 static gboolean sign
= FALSE
;
78 static gboolean no_appstream
= FALSE
;
79 gchar
**remaining_args
= NULL
;
80 gchar
*updateinformation
= NULL
;
81 static gboolean guess_update_information
= FALSE
;
82 gchar
*bintray_user
= NULL
;
83 gchar
*bintray_repo
= NULL
;
84 gchar
*sqfs_comp
= "gzip";
85 gchar
*exclude_file
= NULL
;
86 gchar
*runtime_file
= NULL
;
87 gchar
*sign_args
= NULL
;
88 gchar
*sign_key
= NULL
;
89 gchar
*pathToMksquashfs
= NULL
;
91 // #####################################################################
93 static void die(const char *msg
) {
94 fprintf(stderr
, "%s\n", msg
);
98 /* Function that prints the contents of a squashfs file
99 * using libsquashfuse (#include "squashfuse.h") */
100 int sfs_ls(char* image
) {
101 sqfs_err err
= SQFS_OK
;
105 ssize_t fs_offset
= appimage_get_elf_size(image
);
109 die("failed to read elf size");
111 if ((err
= sqfs_open_image(&fs
, image
, fs_offset
)))
112 die("sqfs_open_image error");
114 if ((err
= sqfs_traverse_open(&trv
, &fs
, sqfs_inode_root(&fs
))))
115 die("sqfs_traverse_open error");
116 while (sqfs_traverse_next(&trv
, &err
)) {
118 printf("%s\n", trv
.path
);
122 die("sqfs_traverse_next error");
123 sqfs_traverse_close(&trv
);
125 sqfs_fd_close(fs
.fd
);
129 /* Generate a squashfs filesystem using mksquashfs on the $PATH
130 * execlp(), execvp(), and execvpe() search on the $PATH */
131 int sfs_mksquashfs(char *source
, char *destination
, int offset
) {
135 // error, failed to fork()
137 } else if (pid
> 0) {
139 waitpid(pid
, &status
, 0);
142 gchar
* offset_string
;
143 offset_string
= g_strdup_printf("%i", offset
);
146 bool use_xz
= strcmp(sqfs_comp
, "xz") >= 0;
149 #ifndef AUXILIARY_FILES_DESTINATION
150 args
[i
++] = "mksquashfs";
152 args
[i
++] = pathToMksquashfs
;
155 args
[i
++] = destination
;
156 args
[i
++] = "-offset";
157 args
[i
++] = offset_string
;
163 args
[i
++] = sqfs_comp
;
165 args
[i
++] = "-root-owned";
166 args
[i
++] = "-noappend";
169 // https://jonathancarter.org/2015/04/06/squashfs-performance-testing/ says:
170 // improved performance by using a 16384 block size with a sacrifice of around 3% more squashfs image space
171 args
[i
++] = "-Xdict-size";
177 // check if ignore file exists and use it if possible
178 if (access(APPIMAGEIGNORE
, F_OK
) >= 0) {
179 printf("Including %s", APPIMAGEIGNORE
);
180 args
[i
++] = "-wildcards";
183 // avoid warning: assignment discards ‘const’ qualifier
184 char* buf
= strdup(APPIMAGEIGNORE
);
188 // if an exclude file has been passed on the command line, should be used, too
189 if (exclude_file
!= 0 && strlen(exclude_file
) > 0) {
190 if (access(exclude_file
, F_OK
) < 0) {
191 printf("WARNING: exclude file %s not found!", exclude_file
);
195 args
[i
++] = "-wildcards";
197 args
[i
++] = exclude_file
;
200 args
[i
++] = "-mkfs-time";
206 printf("mksquashfs commandline: ");
207 for (char** t
= args
; *t
!= 0; t
++) {
213 #ifndef AUXILIARY_FILES_DESTINATION
214 execvp("mksquashfs", args
);
216 execvp(pathToMksquashfs
, args
);
219 perror("execlp"); // exec*() returns only on error
220 return -1; // exec never returns
225 /* Validate desktop file using desktop-file-validate on the $PATH
226 * execlp(), execvp(), and execvpe() search on the $PATH */
227 int validate_desktop_file(char *file
) {
233 printf("could not fork! \n");
236 else if(child_pid
== 0)
238 execlp("desktop-file-validate", "desktop-file-validate", file
, NULL
);
242 waitpid(child_pid
, &statval
, WUNTRACED
| WCONTINUED
);
243 if(WIFEXITED(statval
)){
244 return(WEXITSTATUS(statval
));
250 /* Generate a squashfs filesystem
251 * The following would work if we link to mksquashfs.o after we renamed
252 * main() to mksquashfs_main() in mksquashfs.c but we don't want to actually do
253 * this because squashfs-tools is not under a permissive license
254 * i *nt sfs_mksquashfs(char *source, char *destination) {
255 * char *child_argv[5];
256 * child_argv[0] = NULL;
257 * child_argv[1] = source;
258 * child_argv[2] = destination;
259 * child_argv[3] = "-root-owned";
260 * child_argv[4] = "-noappend";
261 * mksquashfs_main(5, child_argv);
265 /* in-place modification of the string, and assuming the buffer pointed to by
266 * line is large enough to hold the resulting string*/
267 static void replacestr(char *line
, const char *search
, const char *replace
)
271 if ((sp
= strstr(line
, search
)) == NULL
) {
274 int search_len
= strlen(search
);
275 int replace_len
= strlen(replace
);
276 int tail_len
= strlen(sp
+search_len
);
278 memmove(sp
+replace_len
,sp
+search_len
,tail_len
+1);
279 memcpy(sp
, replace
, replace_len
);
281 /* Do it recursively again until no more work to do */
283 if ((sp
= strstr(line
, search
))) {
284 replacestr(line
, search
, replace
);
288 int count_archs(bool* archs
) {
291 for (i
= 0; i
< 4; i
++) {
292 countArchs
+= archs
[i
];
297 gchar
* getArchName(bool* archs
) {
298 if (archs
[fARCH_i386
])
300 else if (archs
[fARCH_x86_64
])
302 else if (archs
[fARCH_arm
])
304 else if (archs
[fARCH_aarch64
])
310 void extract_arch_from_e_machine_field(int16_t e_machine
, const gchar
* sourcename
, bool* archs
) {
311 if (e_machine
== 3) {
312 archs
[fARCH_i386
] = 1;
314 fprintf(stderr
, "%s used for determining architecture i386\n", sourcename
);
317 if (e_machine
== 62) {
318 archs
[fARCH_x86_64
] = 1;
320 fprintf(stderr
, "%s used for determining architecture x86_64\n", sourcename
);
323 if (e_machine
== 40) {
324 archs
[fARCH_arm
] = 1;
326 fprintf(stderr
, "%s used for determining architecture armhf\n", sourcename
);
329 if (e_machine
== 183) {
330 archs
[fARCH_aarch64
] = 1;
332 fprintf(stderr
, "%s used for determining architecture aarch64\n", sourcename
);
336 void extract_arch_from_text(gchar
*archname
, const gchar
* sourcename
, bool* archs
) {
338 archname
= g_strstrip(archname
);
340 replacestr(archname
, "-", "_");
341 replacestr(archname
, " ", "_");
342 if (g_ascii_strncasecmp("i386", archname
, 20) == 0
343 || g_ascii_strncasecmp("i486", archname
, 20) == 0
344 || g_ascii_strncasecmp("i586", archname
, 20) == 0
345 || g_ascii_strncasecmp("i686", archname
, 20) == 0
346 || g_ascii_strncasecmp("intel_80386", archname
, 20) == 0
347 || g_ascii_strncasecmp("intel_80486", archname
, 20) == 0
348 || g_ascii_strncasecmp("intel_80586", archname
, 20) == 0
349 || g_ascii_strncasecmp("intel_80686", archname
, 20) == 0
351 archs
[fARCH_i386
] = 1;
353 fprintf(stderr
, "%s used for determining architecture i386\n", sourcename
);
354 } else if (g_ascii_strncasecmp("x86_64", archname
, 20) == 0) {
355 archs
[fARCH_x86_64
] = 1;
357 fprintf(stderr
, "%s used for determining architecture x86_64\n", sourcename
);
358 } else if (g_ascii_strncasecmp("arm", archname
, 20) == 0) {
359 archs
[fARCH_arm
] = 1;
361 fprintf(stderr
, "%s used for determining architecture ARM\n", sourcename
);
362 } else if (g_ascii_strncasecmp("arm_aarch64", archname
, 20) == 0) {
363 archs
[fARCH_aarch64
] = 1;
365 fprintf(stderr
, "%s used for determining architecture ARM aarch64\n", sourcename
);
371 int16_t read_elf_e_machine_field(const gchar
* file_path
) {
372 int16_t e_machine
= 0x00;
374 file
= fopen(file_path
, "rb");
376 fseek(file
, 0x12, SEEK_SET
);
377 fgets((char*) (&e_machine
), 0x02, file
);
384 void guess_arch_of_file(const gchar
*archfile
, bool* archs
) {
385 int16_t e_machine_field
= read_elf_e_machine_field(archfile
);
386 extract_arch_from_e_machine_field(e_machine_field
, archfile
, archs
);
389 void find_arch(const gchar
*real_path
, const gchar
*pattern
, bool* archs
) {
392 dir
= g_dir_open(real_path
, 0, NULL
);
395 while ((entry
= g_dir_read_name(dir
)) != NULL
) {
396 full_name
= g_build_filename(real_path
, entry
, NULL
);
397 if (g_file_test(full_name
, G_FILE_TEST_IS_SYMLINK
)) {
398 } else if (g_file_test(full_name
, G_FILE_TEST_IS_DIR
)) {
399 find_arch(full_name
, pattern
, archs
);
400 } else if (g_file_test(full_name
, G_FILE_TEST_IS_EXECUTABLE
) || g_pattern_match_simple(pattern
, entry
) ) {
401 guess_arch_of_file(full_name
, archs
);
407 g_warning("%s: %s", real_path
, g_strerror(errno
));
411 gchar
* find_first_matching_file_nonrecursive(const gchar
*real_path
, const gchar
*pattern
) {
414 dir
= g_dir_open(real_path
, 0, NULL
);
417 while ((entry
= g_dir_read_name(dir
)) != NULL
) {
418 full_name
= g_build_filename(real_path
, entry
, NULL
);
419 if (g_file_test(full_name
, G_FILE_TEST_IS_REGULAR
)) {
420 if(g_pattern_match_simple(pattern
, entry
))
427 g_warning("%s: %s", real_path
, g_strerror(errno
));
432 gchar
* get_desktop_entry(GKeyFile
*kf
, char *key
) {
433 gchar
*value
= g_key_file_get_string (kf
, "Desktop Entry", key
, NULL
);
435 fprintf(stderr
, "%s entry not found in desktop file\n", key
);
440 bool readFile(char* filename
, int* size
, char** buffer
) {
441 FILE* f
= fopen(filename
, "rb");
448 fseek(f
, 0, SEEK_END
);
449 long fsize
= ftell(f
);
450 fseek(f
, 0, SEEK_SET
);
452 char *indata
= malloc(fsize
);
453 fread(indata
, fsize
, 1, f
);
460 /* run a command outside the current appimage, block environs like LD_LIBRARY_PATH */
461 int run_external(const char *filename
, char *const argv
[]) {
464 g_print("run_external: fork failed");
467 } else if (pid
== 0) {
468 // blocks env defined in resources/AppRun
469 unsetenv("LD_LIBRARY_PATH");
470 unsetenv("PYTHONPATH");
471 unsetenv("XDG_DATA_DIRS");
473 unsetenv("GSETTINGS_SCHEMA_DIR");
474 unsetenv("QT_PLUGIN_PATH");
476 execv(filename
, argv
);
477 // execv(3) returns, indicating error
478 g_print("run_external: subprocess execv(3) got error %s", g_strerror(errno
));
482 if (waitpid(pid
, &wstatus
, 0) == -1) {
483 g_print("run_external: wait failed");
486 if (WIFEXITED(wstatus
) && (WEXITSTATUS(wstatus
) == 0)) {
489 g_print("run_external: subprocess exited with status %d", WEXITSTATUS(wstatus
));
495 // #####################################################################
497 static GOptionEntry entries
[] =
499 { "list", 'l', 0, G_OPTION_ARG_NONE
, &list
, "List files in SOURCE AppImage", NULL
},
500 { "updateinformation", 'u', 0, G_OPTION_ARG_STRING
, &updateinformation
, "Embed update information STRING; if zsyncmake is installed, generate zsync file", NULL
},
501 { "guess", 'g', 0, G_OPTION_ARG_NONE
, &guess_update_information
, "Guess update information based on Travis CI or GitLab environment variables", NULL
},
502 { "bintray-user", 0, 0, G_OPTION_ARG_STRING
, &bintray_user
, "Bintray user name", NULL
},
503 { "bintray-repo", 0, 0, G_OPTION_ARG_STRING
, &bintray_repo
, "Bintray repository", NULL
},
504 { "version", 0, 0, G_OPTION_ARG_NONE
, &showVersionOnly
, "Show version number", NULL
},
505 { "verbose", 'v', 0, G_OPTION_ARG_NONE
, &verbose
, "Produce verbose output", NULL
},
506 { "sign", 's', 0, G_OPTION_ARG_NONE
, &sign
, "Sign with gpg[2]", NULL
},
507 { "comp", 0, 0, G_OPTION_ARG_STRING
, &sqfs_comp
, "Squashfs compression", NULL
},
508 { "no-appstream", 'n', 0, G_OPTION_ARG_NONE
, &no_appstream
, "Do not check AppStream metadata", NULL
},
509 { "exclude-file", 0, 0, G_OPTION_ARG_STRING
, &exclude_file
, _exclude_file_desc
, NULL
},
510 { "runtime-file", 0, 0, G_OPTION_ARG_STRING
, &runtime_file
, "Runtime file to use", NULL
},
511 { "sign-key", 0, 0, G_OPTION_ARG_STRING
, &sign_key
, "Key ID to use for gpg[2] signatures", NULL
},
512 { "sign-args", 0, 0, G_OPTION_ARG_STRING
, &sign_args
, "Extra arguments to use when signing with gpg[2]", NULL
},
513 { G_OPTION_REMAINING
, 0, 0, G_OPTION_ARG_FILENAME_ARRAY
, &remaining_args
, NULL
, NULL
},
518 main (int argc
, char *argv
[])
521 /* Parse Travis CI environment variables.
522 * https://docs.travis-ci.com/user/environment-variables/#Default-Environment-Variables
523 * TRAVIS_COMMIT: The commit that the current build is testing.
524 * TRAVIS_REPO_SLUG: The slug (in form: owner_name/repo_name) of the repository currently being built.
525 * TRAVIS_TAG: If the current build is for a git tag, this variable is set to the tag’s name.
526 * We cannot use g_environ_getenv (g_get_environ() since it is too new for CentOS 6 */
527 // char* travis_commit;
528 // travis_commit = getenv("TRAVIS_COMMIT");
529 char* travis_repo_slug
;
530 travis_repo_slug
= getenv("TRAVIS_REPO_SLUG");
532 travis_tag
= getenv("TRAVIS_TAG");
533 char* travis_pull_request
;
534 travis_pull_request
= getenv("TRAVIS_PULL_REQUEST");
535 /* https://github.com/probonopd/uploadtool */
537 github_token
= getenv("GITHUB_TOKEN");
539 /* Parse GitLab CI environment variables.
540 * https://docs.gitlab.com/ee/ci/variables/#predefined-variables-environment-variables
541 * echo "${CI_PROJECT_URL}/-/jobs/artifacts/${CI_COMMIT_REF_NAME}/raw/QtQuickApp-x86_64.AppImage?job=${CI_JOB_NAME}"
543 char* CI_PROJECT_URL
;
544 CI_PROJECT_URL
= getenv("CI_PROJECT_URL");
545 char* CI_COMMIT_REF_NAME
;
546 CI_COMMIT_REF_NAME
= getenv("CI_COMMIT_REF_NAME"); // The branch or tag name for which project is built
548 CI_JOB_NAME
= getenv("CI_JOB_NAME"); // The name of the job as defined in .gitlab-ci.yml
550 /* Parse OWD environment variable.
551 * If it is available then cd there. It is the original CWD prior to running AppRun */
552 char* owd_env
= NULL
;
553 owd_env
= getenv("OWD");
556 ret
= chdir(owd_env
);
558 fprintf(stderr
, "Could not cd into %s\n", owd_env
);
563 GError
*error
= NULL
;
564 GOptionContext
*context
;
565 char command
[PATH_MAX
];
567 // initialize help text of argument
568 sprintf(_exclude_file_desc
, "Uses given file as exclude file for mksquashfs, in addition to %s.", APPIMAGEIGNORE
);
570 context
= g_option_context_new ("SOURCE [DESTINATION] - Generate, extract, and inspect AppImages");
571 g_option_context_add_main_entries (context
, entries
, NULL
);
572 // g_option_context_add_group (context, gtk_get_option_group (TRUE));
573 if (!g_option_context_parse (context
, &argc
, &argv
, &error
))
575 fprintf(stderr
, "Option parsing failed: %s\n", error
->message
);
581 "appimagetool, %s (commit %s), build %s built on %s\n",
582 RELEASE_NAME
, GIT_COMMIT
, BUILD_NUMBER
, BUILD_DATE
585 // always show version, but exit immediately if only the version number was requested
589 if(!((0 == strcmp(sqfs_comp
, "gzip")) || (0 ==strcmp(sqfs_comp
, "xz"))))
590 die("Only gzip (faster execution, larger files) and xz (slower execution, smaller files) compression is supported at the moment. Let us know if there are reasons for more, should be easy to add. You could help the project by doing some systematic size/performance measurements. Watch for size, execution speed, and zsync delta size.");
591 /* Check for dependencies here. Better fail early if they are not present. */
592 if(! g_find_program_in_path ("file"))
593 die("file command is missing but required, please install it");
594 #ifndef AUXILIARY_FILES_DESTINATION
595 if(! g_find_program_in_path ("mksquashfs"))
596 die("mksquashfs command is missing but required, please install it");
599 // build path relative to appimagetool binary
600 char *appimagetoolDirectory
= dirname(realpath("/proc/self/exe", NULL
));
601 if (!appimagetoolDirectory
) {
602 g_print("Could not access /proc/self/exe\n");
606 pathToMksquashfs
= g_build_filename(appimagetoolDirectory
, "..", AUXILIARY_FILES_DESTINATION
, "mksquashfs", NULL
);
608 if (!g_file_test(pathToMksquashfs
, G_FILE_TEST_EXISTS
| G_FILE_TEST_IS_EXECUTABLE
)) {
609 g_printf("No such file or directory: %s\n", pathToMksquashfs
);
610 g_free(pathToMksquashfs
);
615 if(! g_find_program_in_path ("desktop-file-validate"))
616 die("desktop-file-validate command is missing, please install it");
617 if(! g_find_program_in_path ("zsyncmake"))
618 g_print("WARNING: zsyncmake command is missing, please install it if you want to use binary delta updates\n");
620 if(! g_find_program_in_path ("appstreamcli"))
621 g_print("WARNING: appstreamcli command is missing, please install it if you want to use AppStream metadata\n");
622 if(! g_find_program_in_path ("gpg2") && ! g_find_program_in_path ("gpg"))
623 g_print("WARNING: gpg2 or gpg command is missing, please install it if you want to create digital signatures\n");
624 if(! g_find_program_in_path ("sha256sum") && ! g_find_program_in_path ("shasum"))
625 g_print("WARNING: sha256sum or shasum command is missing, please install it if you want to create digital signatures\n");
627 if(!&remaining_args
[0])
628 die("SOURCE is missing");
630 /* If in list mode */
632 sfs_ls(remaining_args
[0]);
636 /* If the first argument is a directory, then we assume that we should package it */
637 if (g_file_test(remaining_args
[0], G_FILE_TEST_IS_DIR
)) {
638 /* Parse VERSION environment variable.
639 * We cannot use g_environ_getenv (g_get_environ() since it is too new for CentOS 6
640 * Also, if VERSION is not set and -g is called and if git is on the path, use
641 * git rev-parse --short HEAD
642 * TODO: Might also want to somehow make use of
643 * git rev-parse --abbrev-ref HEAD
644 * git log -1 --format=%ci */
645 gchar
* version_env
= getenv("VERSION");
647 if (guess_update_information
) {
648 char* gitPath
= g_find_program_in_path("git");
650 if (gitPath
!= NULL
) {
651 if (version_env
== NULL
) {
652 GError
* error
= NULL
;
655 char command_line
[] = "git rev-parse --short HEAD";
657 // *not* the exit code! must be interpreted via g_spawn_check_exit_status!
658 int exit_status
= -1;
660 // g_spawn_command_line_sync returns whether the program succeeded
661 // its return value is buggy, hence we're using g_spawn_check_exit_status to check for errors
662 g_spawn_command_line_sync(command_line
, &out
, NULL
, &exit_status
, &error
);
664 // g_spawn_command_line_sync might have set error already, in that case we don't want to overwrite
665 if (error
!= NULL
|| !g_spawn_check_exit_status(exit_status
, &error
)) {
667 g_printerr("Failed to run 'git rev-parse --short HEAD, but failed to interpret GLib error state: %d\n", exit_status
);
669 g_printerr("Failed to run 'git rev-parse --short HEAD: %s (code %d)\n", error
->message
, error
->code
);
672 version_env
= g_strstrip(out
);
674 if (version_env
!= NULL
) {
675 g_printerr("NOTE: Using the output of 'git rev-parse --short HEAD' as the version:\n");
676 g_printerr(" %s\n", version_env
);
677 g_printerr(" Please set the $VERSION environment variable if this is not intended\n");
687 char source
[PATH_MAX
];
688 realpath(remaining_args
[0], source
);
690 /* Check if *.desktop file is present in source AppDir */
691 gchar
*desktop_file
= find_first_matching_file_nonrecursive(source
, "*.desktop");
692 if(desktop_file
== NULL
){
693 die("Desktop file not found, aborting");
696 fprintf (stdout
, "Desktop file: %s\n", desktop_file
);
698 if(g_find_program_in_path ("desktop-file-validate")) {
699 if(validate_desktop_file(desktop_file
) != 0){
700 fprintf(stderr
, "ERROR: Desktop file contains errors. Please fix them. Please see\n");
701 fprintf(stderr
, " https://standards.freedesktop.org/desktop-entry-spec/1.0/n");
702 die(" for more information.");
706 /* Read information from .desktop file */
707 GKeyFile
*kf
= g_key_file_new ();
708 if (!g_key_file_load_from_file (kf
, desktop_file
, G_KEY_FILE_KEEP_TRANSLATIONS
| G_KEY_FILE_KEEP_COMMENTS
, NULL
))
709 die(".desktop file cannot be parsed");
710 if (!get_desktop_entry(kf
, "Categories"))
711 die(".desktop file is missing a Categories= key");
714 fprintf (stderr
,"Name: %s\n", get_desktop_entry(kf
, "Name"));
715 fprintf (stderr
,"Icon: %s\n", get_desktop_entry(kf
, "Icon"));
716 fprintf (stderr
,"Exec: %s\n", get_desktop_entry(kf
, "Exec"));
717 fprintf (stderr
,"Comment: %s\n", get_desktop_entry(kf
, "Comment"));
718 fprintf (stderr
,"Type: %s\n", get_desktop_entry(kf
, "Type"));
719 fprintf (stderr
,"Categories: %s\n", get_desktop_entry(kf
, "Categories"));
722 /* Determine the architecture */
723 bool archs
[4] = {0, 0, 0, 0};
724 extract_arch_from_text(getenv("ARCH"), "Environmental variable ARCH", archs
);
725 if (count_archs(archs
) != 1) {
726 /* If no $ARCH variable is set check a file */
727 /* We use the next best .so that we can find to determine the architecture */
728 find_arch(source
, "*.so.*", archs
);
729 int countArchs
= count_archs(archs
);
730 if (countArchs
!= 1) {
732 fprintf(stderr
, "Unable to guess the architecture of the AppDir source directory \"%s\"\n", remaining_args
[0]);
734 fprintf(stderr
, "More than one architectures were found of the AppDir source directory \"%s\"\n", remaining_args
[0]);
735 fprintf(stderr
, "A valid architecture with the ARCH environmental variable should be provided\ne.g. ARCH=x86_64 %s", argv
[0]),
739 gchar
* arch
= getArchName(archs
);
740 fprintf(stderr
, "Using architecture %s\n", arch
);
743 char app_name_for_filename
[PATH_MAX
];
744 sprintf(app_name_for_filename
, "%s", get_desktop_entry(kf
, "Name"));
745 replacestr(app_name_for_filename
, " ", "_");
748 fprintf (stderr
,"App name for filename: %s\n", app_name_for_filename
);
750 if (remaining_args
[1]) {
751 destination
= remaining_args
[1];
753 /* No destination has been specified, to let's construct one
754 * TODO: Find out the architecture and use a $VERSION that might be around in the env */
755 char dest_path
[PATH_MAX
];
757 // if $VERSION is specified, we embed it into the filename
758 if (version_env
!= NULL
) {
759 sprintf(dest_path
, "%s-%s-%s.AppImage", app_name_for_filename
, version_env
, arch
);
761 sprintf(dest_path
, "%s-%s.AppImage", app_name_for_filename
, arch
);
764 destination
= strdup(dest_path
);
765 replacestr(destination
, " ", "_");
768 // if $VERSION is specified, we embed its value into the desktop file
769 if (version_env
!= NULL
) {
770 g_key_file_set_string(kf
, G_KEY_FILE_DESKTOP_GROUP
, "X-AppImage-Version", version_env
);
772 if (!g_key_file_save_to_file(kf
, desktop_file
, NULL
)) {
773 fprintf(stderr
, "Could not save modified desktop file\n");
778 fprintf (stdout
, "%s should be packaged as %s\n", source
, destination
);
779 /* Check if the Icon file is how it is expected */
780 gchar
* icon_name
= get_desktop_entry(kf
, "Icon");
781 gchar
* icon_file_path
= NULL
;
782 gchar
* icon_file_png
;
783 gchar
* icon_file_svg
;
784 gchar
* icon_file_xpm
;
785 icon_file_png
= g_strdup_printf("%s/%s.png", source
, icon_name
);
786 icon_file_svg
= g_strdup_printf("%s/%s.svg", source
, icon_name
);
787 icon_file_xpm
= g_strdup_printf("%s/%s.xpm", source
, icon_name
);
788 if (g_file_test(icon_file_png
, G_FILE_TEST_IS_REGULAR
)) {
789 icon_file_path
= icon_file_png
;
790 } else if(g_file_test(icon_file_svg
, G_FILE_TEST_IS_REGULAR
)) {
791 icon_file_path
= icon_file_svg
;
792 } else if(g_file_test(icon_file_xpm
, G_FILE_TEST_IS_REGULAR
)) {
793 icon_file_path
= icon_file_xpm
;
795 fprintf (stderr
, "%s{.png,.svg,.xpm} defined in desktop file but not found\n", icon_name
);
796 fprintf (stderr
, "For example, you could put a 256x256 pixel png into\n");
797 gchar
*icon_name_with_png
= g_strconcat(icon_name
, ".png", NULL
);
798 gchar
*example_path
= g_build_filename(source
, "/", icon_name_with_png
, NULL
);
799 fprintf (stderr
, "%s\n", example_path
);
803 /* Check if .DirIcon is present in source AppDir */
804 gchar
*diricon_path
= g_build_filename(source
, ".DirIcon", NULL
);
806 if (! g_file_test(diricon_path
, G_FILE_TEST_EXISTS
)){
807 fprintf (stderr
, "Deleting pre-existing .DirIcon\n");
808 g_unlink(diricon_path
);
810 if (! g_file_test(diricon_path
, G_FILE_TEST_IS_REGULAR
)){
811 fprintf (stderr
, "Creating .DirIcon symlink based on information from desktop file\n");
812 int res
= symlink(basename(icon_file_path
), diricon_path
);
814 die("Could not symlink .DirIcon");
817 /* Check if AppStream upstream metadata is present in source AppDir */
819 char application_id
[PATH_MAX
];
820 sprintf (application_id
, "%s", basename(desktop_file
));
821 replacestr(application_id
, ".desktop", ".appdata.xml");
822 gchar
*appdata_path
= g_build_filename(source
, "/usr/share/metainfo/", application_id
, NULL
);
823 if (! g_file_test(appdata_path
, G_FILE_TEST_IS_REGULAR
)){
824 fprintf (stderr
, "WARNING: AppStream upstream metadata is missing, please consider creating it\n");
825 fprintf (stderr
, " in usr/share/metainfo/%s\n", application_id
);
826 fprintf (stderr
, " Please see https://www.freedesktop.org/software/appstream/docs/chap-Quickstart.html#sect-Quickstart-DesktopApps\n");
827 fprintf (stderr
, " for more information or use the generator at http://output.jsbin.com/qoqukof.\n");
829 fprintf (stderr
, "AppStream upstream metadata found in usr/share/metainfo/%s\n", application_id
);
830 /* Use ximion's appstreamcli to make sure that desktop file and appdata match together */
831 if(g_find_program_in_path ("appstreamcli")) {
838 g_print("Trying to validate AppStream information with the appstreamcli tool\n");
839 g_print("In case of issues, please refer to https://github.com/ximion/appstream\n");
840 int ret
= run_external(g_find_program_in_path ("appstreamcli"), args
);
842 die("Failed to validate AppStream information with appstreamcli");
844 /* It seems that hughsie's appstream-util does additional validations */
845 if(g_find_program_in_path ("appstream-util")) {
852 g_print("Trying to validate AppStream information with the appstream-util tool\n");
853 g_print("In case of issues, please refer to https://github.com/hughsie/appstream-glib\n");
854 int ret
= run_external(g_find_program_in_path ("appstream-util"), args
);
856 die("Failed to validate AppStream information with appstream-util");
861 /* Upstream mksquashfs can currently not start writing at an offset,
862 * so we need a patched one. https://github.com/plougher/squashfs-tools/pull/13
863 * should hopefully change that. */
865 fprintf (stderr
, "Generating squashfs...\n");
868 bool using_external_data
= false;
869 if (runtime_file
!= NULL
) {
870 if (!readFile(runtime_file
, &size
, &data
))
871 die("Unable to load provided runtime file");
872 using_external_data
= true;
874 #ifdef HAVE_BINARY_RUNTIME
875 /* runtime is embedded into this executable
876 * http://stupefydeveloper.blogspot.de/2008/08/cc-embed-binary-data-into-elf.html */
880 die("No runtime file was provided");
884 printf("Size of the embedded runtime: %d bytes\n", size
);
886 int result
= sfs_mksquashfs(source
, destination
, size
);
888 die("sfs_mksquashfs error");
890 fprintf (stderr
, "Embedding ELF...\n");
891 FILE *fpdst
= fopen(destination
, "rb+");
893 die("Not able to open the AppImage for writing, aborting");
896 fseek(fpdst
, 0, SEEK_SET
);
897 fwrite(data
, size
, 1, fpdst
);
899 if (using_external_data
)
902 fprintf (stderr
, "Marking the AppImage as executable...\n");
903 if (chmod (destination
, 0755) < 0) {
904 printf("Could not set executable bit, aborting\n");
908 if(bintray_user
!= NULL
){
909 if(bintray_repo
!= NULL
){
911 sprintf(buf
, "bintray-zsync|%s|%s|%s|%s-_latestVersion-%s.AppImage.zsync", bintray_user
, bintray_repo
, app_name_for_filename
, app_name_for_filename
, arch
);
912 updateinformation
= buf
;
913 printf("%s\n", updateinformation
);
917 /* If the user has not provided update information but we know this is a Travis CI build,
918 * then fill in update information based on TRAVIS_REPO_SLUG */
919 if(guess_update_information
){
920 if(travis_repo_slug
){
922 printf("Will not guess update information since $GITHUB_TOKEN is missing,\n");
923 if(0 != strcmp(travis_pull_request
, "false")){
924 printf("please set it in the Travis CI Repository Settings for this project.\n");
925 printf("You can get one from https://github.com/settings/tokens\n");
927 printf("which is expected since this is a pull request\n");
930 gchar
*zsyncmake_path
= g_find_program_in_path ("zsyncmake");
933 gchar
**parts
= g_strsplit (travis_repo_slug
, "/", 2);
934 /* https://github.com/AppImage/AppImageSpec/blob/master/draft.md#github-releases
935 * gh-releases-zsync|probono|AppImages|latest|Subsurface*-x86_64.AppImage.zsync */
936 gchar
*channel
= "continuous";
937 if(travis_tag
!= NULL
){
938 if((strcmp(travis_tag
, "") != 0) && (strcmp(travis_tag
, "continuous") != 0)) {
942 sprintf(buf
, "gh-releases-zsync|%s|%s|%s|%s*-%s.AppImage.zsync", parts
[0], parts
[1], channel
, app_name_for_filename
, arch
);
943 updateinformation
= buf
;
944 printf("Guessing update information based on $TRAVIS_TAG=%s and $TRAVIS_REPO_SLUG=%s\n", travis_tag
, travis_repo_slug
);
945 printf("%s\n", updateinformation
);
947 printf("Will not guess update information since zsyncmake is missing\n");
950 } else if(CI_COMMIT_REF_NAME
){
951 // ${CI_PROJECT_URL}/-/jobs/artifacts/${CI_COMMIT_REF_NAME}/raw/QtQuickApp-x86_64.AppImage?job=${CI_JOB_NAME}
952 gchar
*zsyncmake_path
= g_find_program_in_path ("zsyncmake");
955 sprintf(buf
, "zsync|%s/-/jobs/artifacts/%s/raw/%s-%s.AppImage.zsync?job=%s", CI_PROJECT_URL
, CI_COMMIT_REF_NAME
, app_name_for_filename
, arch
, CI_JOB_NAME
);
956 updateinformation
= buf
;
957 printf("Guessing update information based on $CI_COMMIT_REF_NAME=%s and $CI_JOB_NAME=%s\n", CI_COMMIT_REF_NAME
, CI_JOB_NAME
);
958 printf("%s\n", updateinformation
);
960 printf("Will not guess update information since zsyncmake is missing\n");
965 /* If updateinformation was provided, then we check and embed it */
966 if(updateinformation
!= NULL
){
967 if(!g_str_has_prefix(updateinformation
,"zsync|"))
968 if(!g_str_has_prefix(updateinformation
,"bintray-zsync|"))
969 if(!g_str_has_prefix(updateinformation
,"gh-releases-zsync|"))
970 if(!g_str_has_prefix(updateinformation
,"pling-v1-zsync|"))
971 die("The provided updateinformation is not in a recognized format");
973 gchar
**ui_type
= g_strsplit_set(updateinformation
, "|", -1);
976 printf("updateinformation type: %s\n", ui_type
[0]);
977 /* TODO: Further checking of the updateinformation */
980 unsigned long ui_offset
= 0;
981 unsigned long ui_length
= 0;
983 bool rv
= appimage_get_elf_section_offset_and_length(destination
, ".upd_info", &ui_offset
, &ui_length
);
985 if (!rv
|| ui_offset
== 0 || ui_length
== 0) {
986 die("Could not find section .upd_info in runtime");
990 printf("ui_offset: %lu\n", ui_offset
);
991 printf("ui_length: %lu\n", ui_length
);
994 die("Could not determine offset for updateinformation");
996 if(strlen(updateinformation
)>ui_length
)
997 die("updateinformation does not fit into segment, aborting");
998 FILE *fpdst2
= fopen(destination
, "r+");
1000 die("Not able to open the destination file for writing, aborting");
1001 fseek(fpdst2
, ui_offset
, SEEK_SET
);
1002 // fseek(fpdst2, ui_offset, SEEK_SET);
1003 // fwrite(0x00, 1, 1024, fpdst); // FIXME: Segfaults; why?
1004 // fseek(fpdst, ui_offset, SEEK_SET);
1005 fwrite(updateinformation
, strlen(updateinformation
), 1, fpdst2
);
1010 // calculate and embed MD5 digest
1012 fprintf(stderr
, "Embedding MD5 digest\n");
1014 unsigned long digest_md5_offset
= 0;
1015 unsigned long digest_md5_length
= 0;
1017 bool rv
= appimage_get_elf_section_offset_and_length(destination
, ".digest_md5", &digest_md5_offset
, &digest_md5_length
);
1019 if (!rv
|| digest_md5_offset
== 0 || digest_md5_length
== 0) {
1020 die("Could not find section .digest_md5 in runtime");
1023 static const unsigned long section_size
= 16;
1025 if (digest_md5_length
< section_size
) {
1028 ".digest_md5 section in runtime's ELF header is too small"
1029 "(found %lu bytes, minimum required: %lu bytes)\n",
1030 digest_md5_length
, section_size
1035 char digest_buffer
[section_size
];
1037 if (!appimage_type2_digest_md5(destination
, digest_buffer
)) {
1038 die("Failed to calculate MD5 digest");
1041 FILE* destinationfp
= fopen(destination
, "r+");
1043 if (destinationfp
== NULL
) {
1044 die("Failed to open AppImage for updating");
1047 if (fseek(destinationfp
, digest_md5_offset
, SEEK_SET
) != 0) {
1048 fclose(destinationfp
);
1049 die("Failed to embed MD5 digest: could not seek to section offset");
1052 if (fwrite(digest_buffer
, sizeof(char), section_size
, destinationfp
) != section_size
) {
1053 fclose(destinationfp
);
1054 die("Failed to embed MD5 digest: write failed");
1057 fclose(destinationfp
);
1061 bool using_gpg
= FALSE
;
1062 bool using_shasum
= FALSE
;
1064 /* The user has indicated that he wants to sign */
1065 gchar
* gpg2_path
= g_find_program_in_path("gpg2");
1068 gpg2_path
= g_find_program_in_path("gpg");
1072 gchar
* sha256sum_path
= g_find_program_in_path("sha256sum");
1074 if (!sha256sum_path
) {
1075 sha256sum_path
= g_find_program_in_path("shasum");
1080 fprintf(stderr
, "gpg2 or gpg is not installed, cannot sign\n");
1081 } else if (!sha256sum_path
) {
1082 fprintf(stderr
, "sha256sum or shasum is not installed, cannot sign\n");
1084 fprintf(stderr
, "%s and %s are installed and user requested to sign, "
1085 "hence signing\n", using_gpg
? "gpg" : "gpg2",
1086 using_shasum
? "shasum" : "sha256sum");
1089 digestfile
= br_strcat(destination
, ".digest");
1092 ascfile
= br_strcat(destination
, ".digest.asc");
1094 if (g_file_test(digestfile
, G_FILE_TEST_IS_REGULAR
))
1098 sprintf(command
, "%s -a256 %s", sha256sum_path
, destination
);
1100 sprintf(command
, "%s %s", sha256sum_path
, destination
);
1103 fprintf(stderr
, "%s\n", command
);
1105 fp
= popen(command
, "r");
1108 die(using_shasum
? "shasum command did not succeed" : "sha256sum command did not succeed");
1112 fgets(output
, sizeof(output
) - 1, fp
);
1115 if (pclose(fp
) != 0)
1116 die(using_shasum
? "shasum command did not succeed" : "sha256sum command did not succeed");
1118 // let's use a new scope to avoid polluting the scope too much with temporary variables
1120 // the output of sha256sum is always <hash> <filename>
1121 // we have to split the string, the first item contains the hash then
1122 gchar
** split_result
= g_strsplit_set(output
, " ", -1);
1124 // extract the first item reference, doesn't have to be free-d
1125 const gchar
* digest_only
= split_result
[0];
1127 // print hash which is later signed for debugging purposes
1128 printf("Signing using SHA256 digest: %s\n", digest_only
);
1130 FILE* fpx
= fopen(digestfile
, "w");
1133 fputs(digest_only
, fpx
);
1137 g_strfreev(split_result
);
1142 if (g_file_test(ascfile
, G_FILE_TEST_IS_REGULAR
))
1145 char* key_arg
= NULL
;
1147 if (sign_key
&& strlen(sign_key
) > 0) {
1148 key_arg
= calloc(sizeof(char), strlen(sign_key
) + strlen("--local-user ''"));
1150 if (key_arg
== NULL
)
1151 die("malloc() failed");
1153 strcpy(key_arg
, "--local-user '");
1154 strcat(key_arg
, sign_key
);
1155 strcat(key_arg
, "'");
1159 "%s --batch --detach-sign --armor %s %s %s",
1160 gpg2_path
, key_arg
? key_arg
: "", sign_args
? sign_args
: "", digestfile
1167 fprintf(stderr
, "%s\n", command
);
1169 fp
= popen(command
, "r");
1171 if (pclose(fp
) != 0) {
1172 fprintf(stderr
, "ERROR: %s command did not succeed, could not sign, continuing\n", using_gpg
? "gpg" : "gpg2");
1177 FILE* destinationfp
= fopen(destination
, "r+");
1179 // calculate signature
1181 unsigned long sig_offset
= 0;
1182 unsigned long sig_length
= 0;
1184 bool rv
= appimage_get_elf_section_offset_and_length(destination
, ".sha256_sig", &sig_offset
,
1187 if (!rv
|| sig_offset
== 0 || sig_length
== 0) {
1188 die("Could not find section .sha256_sig in runtime");
1192 printf("sig_offset: %lu\n", sig_offset
);
1193 printf("sig_length: %lu\n", sig_length
);
1196 if (sig_offset
== 0) {
1197 die("Could not determine offset for signature");
1200 if (destinationfp
== NULL
)
1201 die("Not able to open the destination file for writing, aborting");
1203 // if(strlen(updateinformation)>sig_length)
1204 // die("signature does not fit into segment, aborting");
1206 fseek(destinationfp
, sig_offset
, SEEK_SET
);
1208 FILE* ascfilefp
= fopen(ascfile
, "rb");
1210 if (ascfilefp
== NULL
) {
1211 die("Not able to open the asc file for reading, aborting");
1214 static const int bufsize
= 1024;
1215 char buffer
[bufsize
];
1217 size_t totalBytesRead
= 0;
1219 while (!feof(ascfilefp
)) {
1220 size_t bytesRead
= fread(buffer
, sizeof(char), bufsize
, ascfilefp
);
1221 totalBytesRead
+= bytesRead
;
1223 if (totalBytesRead
> sig_length
) {
1224 die("Error: cannot embed key in AppImage: size exceeds reserved ELF section size");
1227 size_t bytesWritten
= fwrite(buffer
, sizeof(char), bytesRead
, destinationfp
);
1229 if (bytesRead
!= bytesWritten
) {
1231 sprintf(message
, "Bytes read and written differ: %lu != %lu", (long unsigned) bytesRead
,
1232 (long unsigned) bytesWritten
);
1238 if (g_file_test(digestfile
, G_FILE_TEST_IS_REGULAR
))
1241 if (sign_key
== NULL
|| strlen(sign_key
) > 0) {
1242 // read which key was used to sign from signature
1243 sprintf(command
, "%s --batch --list-packets %s", gpg2_path
, ascfile
);
1245 fp
= popen(command
, "r");
1248 die("Failed to call gpg[2] to detect signature's key ID");
1251 size_t bytesRead
= fread(buffer
, sizeof(char), bufsize
, fp
);
1253 char* keyid_pos
= strstr(buffer
, "keyid");
1255 if (keyid_pos
== NULL
)
1258 char* keyIDBegin
= keyid_pos
+ strlen("keyid ");
1259 char* endOfKeyID
= strstr(keyIDBegin
, "\n");
1261 sign_key
= calloc(endOfKeyID
- keyIDBegin
, sizeof(char));
1262 memcpy(sign_key
, keyIDBegin
, endOfKeyID
- keyIDBegin
);
1265 // read rest of process input to avoid broken pipe error
1267 fread(buffer
, sizeof(char), bufsize
, fp
);
1270 int retval
= pclose(fp
);
1274 die("Failed to call gpg[2] to detect signature's key ID");
1277 if (g_file_test(ascfile
, G_FILE_TEST_IS_REGULAR
))
1281 // export key and write into section
1283 sprintf(command
, "%s --batch --export --armor %s", gpg2_path
, sign_key
);
1285 unsigned long key_offset
= 0, key_length
= 0;
1287 bool rv
= appimage_get_elf_section_offset_and_length(destination
, ".sig_key", &key_offset
, &key_length
);
1290 printf("key_offset: %lu\n", key_offset
);
1291 printf("key_length: %lu\n", key_length
);
1294 if (!rv
|| key_offset
== 0 || key_length
== 0) {
1295 die("Could not find section .sig_key in runtime");
1298 fseek(destinationfp
, key_offset
, SEEK_SET
);
1300 fp
= popen(command
, "r");
1303 die("Failed to call gpg[2] to export the signature key");
1305 static const int bufsize
= 1024;
1306 char buffer
[bufsize
];
1308 size_t totalBytesRead
= 0;
1310 size_t bytesRead
= fread(buffer
, sizeof(char), bufsize
, fp
);
1311 totalBytesRead
+= bytesRead
;
1313 if (totalBytesRead
> key_length
) {
1314 // read rest of process input to avoid broken pipe error
1316 fread(buffer
, sizeof(char), bufsize
, fp
);
1320 die("Error: cannot embed key in AppImage: size exceeds reserved ELF section size");
1323 size_t bytesWritten
= fwrite(buffer
, sizeof(char), bytesRead
, destinationfp
);
1325 if (bytesRead
!= bytesWritten
) {
1327 sprintf(message
, "Error: Bytes read and written differ: %lu != %lu",
1328 (long unsigned) bytesRead
, (long unsigned) bytesWritten
);
1333 int exportexitcode
= pclose(fp
);
1336 if (exportexitcode
!= 0) {
1338 sprintf(message
, "GPG key export failed: exit code %d", exportexitcode
);
1342 fclose(destinationfp
);
1348 /* If updateinformation was provided, then we also generate the zsync file (after having signed the AppImage) */
1349 if (updateinformation
!= NULL
) {
1350 gchar
* zsyncmake_path
= g_find_program_in_path("zsyncmake");
1351 if (!zsyncmake_path
) {
1352 fprintf(stderr
, "zsyncmake is not installed/bundled, skipping\n");
1354 fprintf(stderr
, "zsyncmake is available and updateinformation is provided, "
1355 "hence generating zsync file\n");
1360 fprintf(stderr
, "%s %s -u %s", zsyncmake_path
, destination
, basename(destination
));
1364 die("fork() failed");
1365 } else if (pid
== 0) {
1366 char* zsyncmake_command
[] = {zsyncmake_path
, destination
, "-u", basename(destination
), NULL
};
1368 // redirect stdout/stderr to /dev/null
1369 int fd
= open("/dev/null", O_WRONLY
);
1375 execv(zsyncmake_path
, zsyncmake_command
);
1377 die("execv() should not return");
1381 if (waitpid(pid
, &exitstatus
, 0) == -1) {
1382 perror("waitpid() failed");
1386 if (WEXITSTATUS(exitstatus
) != 0)
1387 die("zsyncmake command did not succeed");
1392 fprintf(stderr
, "Success\n\n");
1393 fprintf(stderr
, "Please consider submitting your AppImage to AppImageHub, the crowd-sourced\n");
1394 fprintf(stderr
, "central directory of available AppImages, by opening a pull request\n");
1395 fprintf(stderr
, "at https://github.com/AppImage/appimage.github.io\n");
1398 } else if (g_file_test(remaining_args
[0], G_FILE_TEST_IS_REGULAR
)) {
1399 /* If the first argument is a regular file, then we assume that we should unpack it */
1400 fprintf(stdout
, "%s is a file, assuming it is an AppImage and should be unpacked\n", remaining_args
[0]);
1401 die("To be implemented");
1404 fprintf(stderr
, "Error: no such file or directory: %s\n", remaining_args
[0]);
1408 // should never be reached