1 /**************************************************************************
3 * Copyright (c) 2004-18 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>
42 #include "squashfuse.h"
44 #include <sys/types.h>
57 #include "appimage/appimage.h"
60 #define HAVE_BINARY_RUNTIME
61 extern char runtime
[];
62 extern unsigned int runtime_len
;
72 static gchar
const APPIMAGEIGNORE
[] = ".appimageignore";
73 static char _exclude_file_desc
[256];
75 static gboolean list
= FALSE
;
76 static gboolean verbose
= FALSE
;
77 static gboolean showVersionOnly
= FALSE
;
78 static gboolean sign
= FALSE
;
79 static gboolean no_appstream
= FALSE
;
80 gchar
**remaining_args
= NULL
;
81 gchar
*updateinformation
= NULL
;
82 static gboolean guessupdateinformation
= FALSE
;
83 gchar
*bintray_user
= NULL
;
84 gchar
*bintray_repo
= NULL
;
85 gchar
*sqfs_comp
= "gzip";
86 gchar
*exclude_file
= NULL
;
87 gchar
*runtime_file
= NULL
;
88 gchar
*sign_args
= NULL
;
89 gchar
*sign_key
= NULL
;
90 gchar
*pathToMksquashfs
= NULL
;
92 // #####################################################################
94 static void die(const char *msg
) {
95 fprintf(stderr
, "%s\n", msg
);
99 /* Function that prints the contents of a squashfs file
100 * using libsquashfuse (#include "squashfuse.h") */
101 int sfs_ls(char* image
) {
102 sqfs_err err
= SQFS_OK
;
106 ssize_t fs_offset
= appimage_get_elf_size(image
);
110 die("failed to read elf size");
112 if ((err
= sqfs_open_image(&fs
, image
, fs_offset
)))
113 die("sqfs_open_image error");
115 if ((err
= sqfs_traverse_open(&trv
, &fs
, sqfs_inode_root(&fs
))))
116 die("sqfs_traverse_open error");
117 while (sqfs_traverse_next(&trv
, &err
)) {
119 printf("%s\n", trv
.path
);
123 die("sqfs_traverse_next error");
124 sqfs_traverse_close(&trv
);
126 sqfs_fd_close(fs
.fd
);
130 /* Generate a squashfs filesystem using mksquashfs on the $PATH
131 * execlp(), execvp(), and execvpe() search on the $PATH */
132 int sfs_mksquashfs(char *source
, char *destination
, int offset
) {
136 // error, failed to fork()
138 } else if (pid
> 0) {
140 waitpid(pid
, &status
, 0);
143 gchar
* offset_string
;
144 offset_string
= g_strdup_printf("%i", offset
);
147 bool use_xz
= strcmp(sqfs_comp
, "xz") >= 0;
150 #ifndef AUXILIARY_FILES_DESTINATION
151 args
[i
++] = "mksquashfs";
153 args
[i
++] = pathToMksquashfs
;
156 args
[i
++] = destination
;
157 args
[i
++] = "-offset";
158 args
[i
++] = offset_string
;
164 args
[i
++] = sqfs_comp
;
166 args
[i
++] = "-root-owned";
167 args
[i
++] = "-noappend";
170 // https://jonathancarter.org/2015/04/06/squashfs-performance-testing/ says:
171 // improved performance by using a 16384 block size with a sacrifice of around 3% more squashfs image space
172 args
[i
++] = "-Xdict-size";
178 // check if ignore file exists and use it if possible
179 if (access(APPIMAGEIGNORE
, F_OK
) >= 0) {
180 printf("Including %s", APPIMAGEIGNORE
);
181 args
[i
++] = "-wildcards";
184 // avoid warning: assignment discards ‘const’ qualifier
185 char* buf
= strdup(APPIMAGEIGNORE
);
189 // if an exclude file has been passed on the command line, should be used, too
190 if (exclude_file
!= 0 && strlen(exclude_file
) > 0) {
191 if (access(exclude_file
, F_OK
) < 0) {
192 printf("WARNING: exclude file %s not found!", exclude_file
);
196 args
[i
++] = "-wildcards";
198 args
[i
++] = exclude_file
;
201 args
[i
++] = "-mkfs-fixed-time";
207 printf("mksquashfs commandline: ");
208 for (char** t
= args
; *t
!= 0; t
++) {
214 #ifndef AUXILIARY_FILES_DESTINATION
215 execvp("mksquashfs", args
);
217 execvp(pathToMksquashfs
, args
);
220 perror("execlp"); // exec*() returns only on error
221 return -1; // exec never returns
226 /* Validate desktop file using desktop-file-validate on the $PATH
227 * execlp(), execvp(), and execvpe() search on the $PATH */
228 int validate_desktop_file(char *file
) {
234 printf("could not fork! \n");
237 else if(child_pid
== 0)
239 execlp("desktop-file-validate", "desktop-file-validate", file
, NULL
);
243 waitpid(child_pid
, &statval
, WUNTRACED
| WCONTINUED
);
244 if(WIFEXITED(statval
)){
245 return(WEXITSTATUS(statval
));
251 /* Generate a squashfs filesystem
252 * The following would work if we link to mksquashfs.o after we renamed
253 * main() to mksquashfs_main() in mksquashfs.c but we don't want to actually do
254 * this because squashfs-tools is not under a permissive license
255 * i *nt sfs_mksquashfs(char *source, char *destination) {
256 * char *child_argv[5];
257 * child_argv[0] = NULL;
258 * child_argv[1] = source;
259 * child_argv[2] = destination;
260 * child_argv[3] = "-root-owned";
261 * child_argv[4] = "-noappend";
262 * mksquashfs_main(5, child_argv);
266 /* in-place modification of the string, and assuming the buffer pointed to by
267 * line is large enough to hold the resulting string*/
268 static void replacestr(char *line
, const char *search
, const char *replace
)
272 if ((sp
= strstr(line
, search
)) == NULL
) {
275 int search_len
= strlen(search
);
276 int replace_len
= strlen(replace
);
277 int tail_len
= strlen(sp
+search_len
);
279 memmove(sp
+replace_len
,sp
+search_len
,tail_len
+1);
280 memcpy(sp
, replace
, replace_len
);
282 /* Do it recursively again until no more work to do */
284 if ((sp
= strstr(line
, search
))) {
285 replacestr(line
, search
, replace
);
289 int count_archs(bool* archs
) {
292 for (i
= 0; i
< 4; i
++) {
293 countArchs
+= archs
[i
];
298 gchar
* getArchName(bool* archs
) {
299 if (archs
[fARCH_i386
])
301 else if (archs
[fARCH_x86_64
])
303 else if (archs
[fARCH_arm
])
305 else if (archs
[fARCH_aarch64
])
306 return "ARM_aarch64";
311 void extract_arch_from_e_machine_field(int16_t e_machine
, const gchar
* sourcename
, bool* archs
) {
312 if (e_machine
== 3) {
313 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;
319 fprintf(stderr
, "%s used for determining architecture x86_64\n", sourcename
);
322 if (e_machine
== 40) {
323 archs
[fARCH_arm
] = 1;
324 fprintf(stderr
, "%s used for determining architecture ARM\n", sourcename
);
327 if (e_machine
== 183) {
328 archs
[fARCH_aarch64
] = 1;
329 fprintf(stderr
, "%s used for determining architecture ARM aarch64\n", sourcename
);
333 void extract_arch_from_text(gchar
*archname
, const gchar
* sourcename
, bool* archs
) {
335 archname
= g_strstrip(archname
);
337 replacestr(archname
, "-", "_");
338 replacestr(archname
, " ", "_");
339 if (g_ascii_strncasecmp("i386", archname
, 20) == 0
340 || g_ascii_strncasecmp("i486", archname
, 20) == 0
341 || g_ascii_strncasecmp("i586", archname
, 20) == 0
342 || g_ascii_strncasecmp("i686", archname
, 20) == 0
343 || g_ascii_strncasecmp("intel_80386", archname
, 20) == 0
344 || g_ascii_strncasecmp("intel_80486", archname
, 20) == 0
345 || g_ascii_strncasecmp("intel_80586", archname
, 20) == 0
346 || g_ascii_strncasecmp("intel_80686", archname
, 20) == 0
348 archs
[fARCH_i386
] = 1;
350 fprintf(stderr
, "%s used for determining architecture i386\n", sourcename
);
351 } else if (g_ascii_strncasecmp("x86_64", archname
, 20) == 0) {
352 archs
[fARCH_x86_64
] = 1;
354 fprintf(stderr
, "%s used for determining architecture x86_64\n", sourcename
);
355 } else if (g_ascii_strncasecmp("arm", archname
, 20) == 0) {
356 archs
[fARCH_arm
] = 1;
358 fprintf(stderr
, "%s used for determining architecture ARM\n", sourcename
);
359 } else if (g_ascii_strncasecmp("arm_aarch64", archname
, 20) == 0) {
360 archs
[fARCH_aarch64
] = 1;
362 fprintf(stderr
, "%s used for determining architecture ARM aarch64\n", sourcename
);
368 int16_t read_elf_e_machine_field(const gchar
* file_path
) {
369 int16_t e_machine
= 0x00;
371 file
= fopen(file_path
, "rb");
373 fseek(file
, 0x12, SEEK_SET
);
374 fgets((char*) (&e_machine
), 0x02, file
);
381 void guess_arch_of_file(const gchar
*archfile
, bool* archs
) {
382 int16_t e_machine_field
= read_elf_e_machine_field(archfile
);
383 extract_arch_from_e_machine_field(e_machine_field
, archfile
, archs
);
386 void find_arch(const gchar
*real_path
, const gchar
*pattern
, bool* archs
) {
389 dir
= g_dir_open(real_path
, 0, NULL
);
392 while ((entry
= g_dir_read_name(dir
)) != NULL
) {
393 full_name
= g_build_filename(real_path
, entry
, NULL
);
394 if (g_file_test(full_name
, G_FILE_TEST_IS_SYMLINK
)) {
395 } else if (g_file_test(full_name
, G_FILE_TEST_IS_DIR
)) {
396 find_arch(full_name
, pattern
, archs
);
397 } else if (g_file_test(full_name
, G_FILE_TEST_IS_EXECUTABLE
) || g_pattern_match_simple(pattern
, entry
) ) {
398 guess_arch_of_file(full_name
, archs
);
404 g_warning("%s: %s", real_path
, g_strerror(errno
));
408 gchar
* find_first_matching_file_nonrecursive(const gchar
*real_path
, const gchar
*pattern
) {
411 dir
= g_dir_open(real_path
, 0, NULL
);
414 while ((entry
= g_dir_read_name(dir
)) != NULL
) {
415 full_name
= g_build_filename(real_path
, entry
, NULL
);
416 if (g_file_test(full_name
, G_FILE_TEST_IS_REGULAR
)) {
417 if(g_pattern_match_simple(pattern
, entry
))
424 g_warning("%s: %s", real_path
, g_strerror(errno
));
429 gchar
* get_desktop_entry(GKeyFile
*kf
, char *key
) {
430 gchar
*value
= g_key_file_get_string (kf
, "Desktop Entry", key
, NULL
);
432 fprintf(stderr
, "%s entry not found in desktop file\n", key
);
437 bool readFile(char* filename
, int* size
, char** buffer
) {
438 FILE* f
= fopen(filename
, "rb");
445 fseek(f
, 0, SEEK_END
);
446 long fsize
= ftell(f
);
447 fseek(f
, 0, SEEK_SET
);
449 char *indata
= malloc(fsize
);
450 fread(indata
, fsize
, 1, f
);
457 // #####################################################################
459 static GOptionEntry entries
[] =
461 { "list", 'l', 0, G_OPTION_ARG_NONE
, &list
, "List files in SOURCE AppImage", NULL
},
462 { "updateinformation", 'u', 0, G_OPTION_ARG_STRING
, &updateinformation
, "Embed update information STRING; if zsyncmake is installed, generate zsync file", NULL
},
463 { "guess", 'g', 0, G_OPTION_ARG_NONE
, &guessupdateinformation
, "Guess update information based on Travis CI or GitLab environment variables", NULL
},
464 { "bintray-user", 0, 0, G_OPTION_ARG_STRING
, &bintray_user
, "Bintray user name", NULL
},
465 { "bintray-repo", 0, 0, G_OPTION_ARG_STRING
, &bintray_repo
, "Bintray repository", NULL
},
466 { "version", 0, 0, G_OPTION_ARG_NONE
, &showVersionOnly
, "Show version number", NULL
},
467 { "verbose", 'v', 0, G_OPTION_ARG_NONE
, &verbose
, "Produce verbose output", NULL
},
468 { "sign", 's', 0, G_OPTION_ARG_NONE
, &sign
, "Sign with gpg[2]", NULL
},
469 { "comp", 0, 0, G_OPTION_ARG_STRING
, &sqfs_comp
, "Squashfs compression", NULL
},
470 { "no-appstream", 'n', 0, G_OPTION_ARG_NONE
, &no_appstream
, "Do not check AppStream metadata", NULL
},
471 { "exclude-file", 0, 0, G_OPTION_ARG_STRING
, &exclude_file
, _exclude_file_desc
, NULL
},
472 { "runtime-file", 0, 0, G_OPTION_ARG_STRING
, &runtime_file
, "Runtime file to use", NULL
},
473 { "sign-key", 0, 0, G_OPTION_ARG_STRING
, &sign_key
, "Key ID to use for gpg[2] signatures", NULL
},
474 { "sign-args", 0, 0, G_OPTION_ARG_STRING
, &sign_args
, "Extra arguments to use when signing with gpg[2]", NULL
},
475 { G_OPTION_REMAINING
, 0, 0, G_OPTION_ARG_FILENAME_ARRAY
, &remaining_args
, NULL
, NULL
},
480 main (int argc
, char *argv
[])
482 /* Parse VERSION environment variable.
483 * We cannot use g_environ_getenv (g_get_environ() since it is too new for CentOS 6 */
485 version_env
= getenv("VERSION");
487 /* Parse Travis CI environment variables.
488 * https://docs.travis-ci.com/user/environment-variables/#Default-Environment-Variables
489 * TRAVIS_COMMIT: The commit that the current build is testing.
490 * TRAVIS_REPO_SLUG: The slug (in form: owner_name/repo_name) of the repository currently being built.
491 * TRAVIS_TAG: If the current build is for a git tag, this variable is set to the tag’s name.
492 * We cannot use g_environ_getenv (g_get_environ() since it is too new for CentOS 6 */
493 // char* travis_commit;
494 // travis_commit = getenv("TRAVIS_COMMIT");
495 char* travis_repo_slug
;
496 travis_repo_slug
= getenv("TRAVIS_REPO_SLUG");
498 travis_tag
= getenv("TRAVIS_TAG");
499 char* travis_pull_request
;
500 travis_pull_request
= getenv("TRAVIS_PULL_REQUEST");
501 /* https://github.com/probonopd/uploadtool */
503 github_token
= getenv("GITHUB_TOKEN");
505 /* Parse GitLab CI environment variables.
506 * https://docs.gitlab.com/ee/ci/variables/#predefined-variables-environment-variables
507 * echo "${CI_PROJECT_URL}/-/jobs/artifacts/${CI_COMMIT_REF_NAME}/raw/QtQuickApp-x86_64.AppImage?job=${CI_JOB_NAME}"
509 char* CI_PROJECT_URL
;
510 CI_PROJECT_URL
= getenv("CI_PROJECT_URL");
511 char* CI_COMMIT_REF_NAME
;
512 CI_COMMIT_REF_NAME
= getenv("CI_COMMIT_REF_NAME"); // The branch or tag name for which project is built
514 CI_JOB_NAME
= getenv("CI_JOB_NAME"); // The name of the job as defined in .gitlab-ci.yml
516 /* Parse OWD environment variable.
517 * If it is available then cd there. It is the original CWD prior to running AppRun */
518 char* owd_env
= NULL
;
519 owd_env
= getenv("OWD");
522 ret
= chdir(owd_env
);
524 fprintf(stderr
, "Could not cd into %s\n", owd_env
);
529 GError
*error
= NULL
;
530 GOptionContext
*context
;
531 char command
[PATH_MAX
];
533 // initialize help text of argument
534 sprintf(_exclude_file_desc
, "Uses given file as exclude file for mksquashfs, in addition to %s.", APPIMAGEIGNORE
);
536 context
= g_option_context_new ("SOURCE [DESTINATION] - Generate, extract, and inspect AppImages");
537 g_option_context_add_main_entries (context
, entries
, NULL
);
538 // g_option_context_add_group (context, gtk_get_option_group (TRUE));
539 if (!g_option_context_parse (context
, &argc
, &argv
, &error
))
541 fprintf(stderr
, "Option parsing failed: %s\n", error
->message
);
547 "appimagetool, %s (commit %s), build %s built on %s\n",
548 RELEASE_NAME
, GIT_COMMIT
, BUILD_NUMBER
, BUILD_DATE
551 // always show version, but exit immediately if only the version number was requested
555 if(!((0 == strcmp(sqfs_comp
, "gzip")) || (0 ==strcmp(sqfs_comp
, "xz"))))
556 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.");
557 /* Check for dependencies here. Better fail early if they are not present. */
558 if(! g_find_program_in_path ("file"))
559 die("file command is missing but required, please install it");
560 #ifndef AUXILIARY_FILES_DESTINATION
561 if(! g_find_program_in_path ("mksquashfs"))
562 die("mksquashfs command is missing but required, please install it");
565 // build path relative to appimagetool binary
566 char *appimagetoolDirectory
= dirname(realpath("/proc/self/exe", NULL
));
567 if (!appimagetoolDirectory
) {
568 g_print("Could not access /proc/self/exe\n");
572 pathToMksquashfs
= g_build_filename(appimagetoolDirectory
, "..", AUXILIARY_FILES_DESTINATION
, "mksquashfs", NULL
);
574 if (!g_file_test(pathToMksquashfs
, G_FILE_TEST_EXISTS
| G_FILE_TEST_IS_EXECUTABLE
)) {
575 g_printf("No such file or directory: %s\n", pathToMksquashfs
);
576 g_free(pathToMksquashfs
);
581 if(! g_find_program_in_path ("desktop-file-validate"))
582 die("desktop-file-validate command is missing, please install it");
583 if(! g_find_program_in_path ("zsyncmake"))
584 g_print("WARNING: zsyncmake command is missing, please install it if you want to use binary delta updates\n");
586 if(! g_find_program_in_path ("appstreamcli"))
587 g_print("WARNING: appstreamcli command is missing, please install it if you want to use AppStream metadata\n");
588 if(! g_find_program_in_path ("gpg2") && ! g_find_program_in_path ("gpg"))
589 g_print("WARNING: gpg2 or gpg command is missing, please install it if you want to create digital signatures\n");
590 if(! g_find_program_in_path ("sha256sum") && ! g_find_program_in_path ("shasum"))
591 g_print("WARNING: sha256sum or shasum command is missing, please install it if you want to create digital signatures\n");
593 if(!&remaining_args
[0])
594 die("SOURCE is missing");
596 /* If in list mode */
598 sfs_ls(remaining_args
[0]);
602 /* If the first argument is a directory, then we assume that we should package it */
603 if (g_file_test (remaining_args
[0], G_FILE_TEST_IS_DIR
)){
605 char source
[PATH_MAX
];
606 realpath(remaining_args
[0], source
);
608 /* Check if *.desktop file is present in source AppDir */
609 gchar
*desktop_file
= find_first_matching_file_nonrecursive(source
, "*.desktop");
610 if(desktop_file
== NULL
){
611 die("Desktop file not found, aborting");
614 fprintf (stdout
, "Desktop file: %s\n", desktop_file
);
616 if(g_find_program_in_path ("desktop-file-validate")) {
617 if(validate_desktop_file(desktop_file
) != 0){
618 fprintf(stderr
, "ERROR: Desktop file contains errors. Please fix them. Please see\n");
619 fprintf(stderr
, " https://standards.freedesktop.org/desktop-entry-spec/latest/\n");
620 die(" for more information.");
624 /* Read information from .desktop file */
625 GKeyFile
*kf
= g_key_file_new ();
626 if (!g_key_file_load_from_file (kf
, desktop_file
, G_KEY_FILE_KEEP_TRANSLATIONS
| G_KEY_FILE_KEEP_COMMENTS
, NULL
))
627 die(".desktop file cannot be parsed");
630 fprintf (stderr
,"Name: %s\n", get_desktop_entry(kf
, "Name"));
631 fprintf (stderr
,"Icon: %s\n", get_desktop_entry(kf
, "Icon"));
632 fprintf (stderr
,"Exec: %s\n", get_desktop_entry(kf
, "Exec"));
633 fprintf (stderr
,"Comment: %s\n", get_desktop_entry(kf
, "Comment"));
634 fprintf (stderr
,"Type: %s\n", get_desktop_entry(kf
, "Type"));
635 fprintf (stderr
,"Categories: %s\n", get_desktop_entry(kf
, "Categories"));
638 /* Determine the architecture */
639 bool archs
[4] = {0, 0, 0, 0};
640 extract_arch_from_text(getenv("ARCH"), "Environmental variable ARCH", archs
);
641 if (count_archs(archs
) != 1) {
642 /* If no $ARCH variable is set check a file */
643 /* We use the next best .so that we can find to determine the architecture */
644 find_arch(source
, "*.so.*", archs
);
645 int countArchs
= count_archs(archs
);
646 if (countArchs
!= 1) {
648 fprintf(stderr
, "Unable to guess the architecture of the AppDir source directory \"%s\"\n", remaining_args
[0]);
650 fprintf(stderr
, "More than one architectures were found of the AppDir source directory \"%s\"\n", remaining_args
[0]);
651 fprintf(stderr
, "A valid architecture with the ARCH environmental variable should be provided\ne.g. ARCH=x86_64 %s", argv
[0]),
655 gchar
* arch
= getArchName(archs
);
656 fprintf(stderr
, "Using architecture %s\n", arch
);
659 char app_name_for_filename
[PATH_MAX
];
660 sprintf(app_name_for_filename
, "%s", get_desktop_entry(kf
, "Name"));
661 replacestr(app_name_for_filename
, " ", "_");
664 fprintf (stderr
,"App name for filename: %s\n", app_name_for_filename
);
666 if (remaining_args
[1]) {
667 destination
= remaining_args
[1];
669 /* No destination has been specified, to let's construct one
670 * TODO: Find out the architecture and use a $VERSION that might be around in the env */
671 char dest_path
[PATH_MAX
];
672 sprintf(dest_path
, "%s-%s.AppImage", app_name_for_filename
, arch
);
674 if (version_env
!= NULL
) {
675 sprintf(dest_path
, "%s-%s-%s.AppImage", app_name_for_filename
, version_env
, arch
);
677 // set VERSION in desktop file and save it
678 g_key_file_set_string(kf
, G_KEY_FILE_DESKTOP_GROUP
, "X-AppImage-Version", version_env
);
680 if (!g_key_file_save_to_file(kf
, desktop_file
, NULL
)) {
681 fprintf(stderr
, "Could not save modified desktop file\n");
686 destination
= strdup(dest_path
);
687 replacestr(destination
, " ", "_");
690 fprintf (stdout
, "%s should be packaged as %s\n", source
, destination
);
691 /* Check if the Icon file is how it is expected */
692 gchar
* icon_name
= get_desktop_entry(kf
, "Icon");
693 gchar
* icon_file_path
= NULL
;
694 gchar
* icon_file_png
;
695 gchar
* icon_file_svg
;
696 gchar
* icon_file_svgz
;
697 gchar
* icon_file_xpm
;
698 icon_file_png
= g_strdup_printf("%s/%s.png", source
, icon_name
);
699 icon_file_svg
= g_strdup_printf("%s/%s.svg", source
, icon_name
);
700 icon_file_svgz
= g_strdup_printf("%s/%s.svgz", source
, icon_name
);
701 icon_file_xpm
= g_strdup_printf("%s/%s.xpm", source
, icon_name
);
702 if (g_file_test(icon_file_png
, G_FILE_TEST_IS_REGULAR
)) {
703 icon_file_path
= icon_file_png
;
704 } else if(g_file_test(icon_file_svg
, G_FILE_TEST_IS_REGULAR
)) {
705 icon_file_path
= icon_file_svg
;
706 } else if(g_file_test(icon_file_svgz
, G_FILE_TEST_IS_REGULAR
)) {
707 icon_file_path
= icon_file_svgz
;
708 } else if(g_file_test(icon_file_xpm
, G_FILE_TEST_IS_REGULAR
)) {
709 icon_file_path
= icon_file_xpm
;
711 fprintf (stderr
, "%s{.png,.svg,.svgz,.xpm} defined in desktop file but not found\n", icon_name
);
712 fprintf (stderr
, "For example, you could put a 256x256 pixel png into\n");
713 gchar
*icon_name_with_png
= g_strconcat(icon_name
, ".png", NULL
);
714 gchar
*example_path
= g_build_filename(source
, "/", icon_name_with_png
, NULL
);
715 fprintf (stderr
, "%s\n", example_path
);
719 /* Check if .DirIcon is present in source AppDir */
720 gchar
*diricon_path
= g_build_filename(source
, ".DirIcon", NULL
);
722 if (! g_file_test(diricon_path
, G_FILE_TEST_EXISTS
)){
723 fprintf (stderr
, "Deleting pre-existing .DirIcon\n");
724 g_unlink(diricon_path
);
726 if (! g_file_test(diricon_path
, G_FILE_TEST_IS_REGULAR
)){
727 fprintf (stderr
, "Creating .DirIcon symlink based on information from desktop file\n");
728 int res
= symlink(basename(icon_file_path
), diricon_path
);
730 die("Could not symlink .DirIcon");
733 /* Check if AppStream upstream metadata is present in source AppDir */
735 char application_id
[PATH_MAX
];
736 sprintf (application_id
, "%s", basename(desktop_file
));
737 replacestr(application_id
, ".desktop", ".appdata.xml");
738 gchar
*appdata_path
= g_build_filename(source
, "/usr/share/metainfo/", application_id
, NULL
);
739 if (! g_file_test(appdata_path
, G_FILE_TEST_IS_REGULAR
)){
740 fprintf (stderr
, "WARNING: AppStream upstream metadata is missing, please consider creating it\n");
741 fprintf (stderr
, " in usr/share/metainfo/%s\n", application_id
);
742 fprintf (stderr
, " Please see https://www.freedesktop.org/software/appstream/docs/chap-Quickstart.html#sect-Quickstart-DesktopApps\n");
743 fprintf (stderr
, " for more information or use the generator at http://output.jsbin.com/qoqukof.\n");
745 fprintf (stderr
, "AppStream upstream metadata found in usr/share/metainfo/%s\n", application_id
);
746 /* Use ximion's appstreamcli to make sure that desktop file and appdata match together */
747 if(g_find_program_in_path ("appstreamcli")) {
748 sprintf (command
, "%s validate-tree %s", g_find_program_in_path ("appstreamcli"), source
);
749 g_print("Trying to validate AppStream information with the appstreamcli tool\n");
750 g_print("In case of issues, please refer to https://github.com/ximion/appstream\n");
751 int ret
= system(command
);
753 die("Failed to validate AppStream information with appstreamcli");
755 /* It seems that hughsie's appstream-util does additional validations */
756 if(g_find_program_in_path ("appstream-util")) {
757 sprintf (command
, "%s validate-relax %s", g_find_program_in_path ("appstream-util"), appdata_path
);
758 g_print("Trying to validate AppStream information with the appstream-util tool\n");
759 g_print("In case of issues, please refer to https://github.com/hughsie/appstream-glib\n");
760 int ret
= system(command
);
762 die("Failed to validate AppStream information with appstream-util");
767 /* Upstream mksquashfs can currently not start writing at an offset,
768 * so we need a patched one. https://github.com/plougher/squashfs-tools/pull/13
769 * should hopefully change that. */
771 fprintf (stderr
, "Generating squashfs...\n");
774 bool using_external_data
= false;
775 if (runtime_file
!= NULL
) {
776 if (!readFile(runtime_file
, &size
, &data
))
777 die("Unable to load provided runtime file");
778 using_external_data
= true;
780 #ifdef HAVE_BINARY_RUNTIME
781 /* runtime is embedded into this executable
782 * http://stupefydeveloper.blogspot.de/2008/08/cc-embed-binary-data-into-elf.html */
786 die("No runtime file was provided");
790 printf("Size of the embedded runtime: %d bytes\n", size
);
792 int result
= sfs_mksquashfs(source
, destination
, size
);
794 die("sfs_mksquashfs error");
796 fprintf (stderr
, "Embedding ELF...\n");
797 FILE *fpdst
= fopen(destination
, "rb+");
799 die("Not able to open the AppImage for writing, aborting");
802 fseek(fpdst
, 0, SEEK_SET
);
803 fwrite(data
, size
, 1, fpdst
);
805 if (using_external_data
)
808 fprintf (stderr
, "Marking the AppImage as executable...\n");
809 if (chmod (destination
, 0755) < 0) {
810 printf("Could not set executable bit, aborting\n");
814 if(bintray_user
!= NULL
){
815 if(bintray_repo
!= NULL
){
817 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
);
818 updateinformation
= buf
;
819 printf("%s\n", updateinformation
);
823 /* If the user has not provided update information but we know this is a Travis CI build,
824 * then fill in update information based on TRAVIS_REPO_SLUG */
825 if(guessupdateinformation
){
826 if(travis_repo_slug
){
828 printf("Will not guess update information since $GITHUB_TOKEN is missing,\n");
829 if(0 != strcmp(travis_pull_request
, "false")){
830 printf("please set it in the Travis CI Repository Settings for this project.\n");
831 printf("You can get one from https://github.com/settings/tokens\n");
833 printf("which is expected since this is a pull request\n");
836 gchar
*zsyncmake_path
= g_find_program_in_path ("zsyncmake");
839 gchar
**parts
= g_strsplit (travis_repo_slug
, "/", 2);
840 /* https://github.com/AppImage/AppImageSpec/blob/master/draft.md#github-releases
841 * gh-releases-zsync|probono|AppImages|latest|Subsurface*-x86_64.AppImage.zsync */
842 gchar
*channel
= "continuous";
843 if(travis_tag
!= NULL
){
844 if((strcmp(travis_tag
, "") != 0) && (strcmp(travis_tag
, "continuous") != 0)) {
848 sprintf(buf
, "gh-releases-zsync|%s|%s|%s|%s*-%s.AppImage.zsync", parts
[0], parts
[1], channel
, app_name_for_filename
, arch
);
849 updateinformation
= buf
;
850 printf("Guessing update information based on $TRAVIS_TAG=%s and $TRAVIS_REPO_SLUG=%s\n", travis_tag
, travis_repo_slug
);
851 printf("%s\n", updateinformation
);
853 printf("Will not guess update information since zsyncmake is missing\n");
856 } else if(CI_COMMIT_REF_NAME
){
857 // ${CI_PROJECT_URL}/-/jobs/artifacts/${CI_COMMIT_REF_NAME}/raw/QtQuickApp-x86_64.AppImage?job=${CI_JOB_NAME}
858 gchar
*zsyncmake_path
= g_find_program_in_path ("zsyncmake");
861 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
);
862 updateinformation
= buf
;
863 printf("Guessing update information based on $CI_COMMIT_REF_NAME=%s and $CI_JOB_NAME=%s\n", CI_COMMIT_REF_NAME
, CI_JOB_NAME
);
864 printf("%s\n", updateinformation
);
866 printf("Will not guess update information since zsyncmake is missing\n");
871 /* If updateinformation was provided, then we check and embed it */
872 if(updateinformation
!= NULL
){
873 if(!g_str_has_prefix(updateinformation
,"zsync|"))
874 if(!g_str_has_prefix(updateinformation
,"bintray-zsync|"))
875 if(!g_str_has_prefix(updateinformation
,"gh-releases-zsync|"))
876 die("The provided updateinformation is not in a recognized format");
878 gchar
**ui_type
= g_strsplit_set(updateinformation
, "|", -1);
881 printf("updateinformation type: %s\n", ui_type
[0]);
882 /* TODO: Further checking of the updateinformation */
885 unsigned long ui_offset
= 0;
886 unsigned long ui_length
= 0;
888 bool rv
= appimage_get_elf_section_offset_and_length(destination
, ".upd_info", &ui_offset
, &ui_length
);
890 if (!rv
|| ui_offset
== 0 || ui_length
== 0) {
891 die("Could not find section .upd_info in runtime");
895 printf("ui_offset: %lu\n", ui_offset
);
896 printf("ui_length: %lu\n", ui_length
);
899 die("Could not determine offset for updateinformation");
901 if(strlen(updateinformation
)>ui_length
)
902 die("updateinformation does not fit into segment, aborting");
903 FILE *fpdst2
= fopen(destination
, "r+");
905 die("Not able to open the destination file for writing, aborting");
906 fseek(fpdst2
, ui_offset
, SEEK_SET
);
907 // fseek(fpdst2, ui_offset, SEEK_SET);
908 // fwrite(0x00, 1, 1024, fpdst); // FIXME: Segfaults; why?
909 // fseek(fpdst, ui_offset, SEEK_SET);
910 fwrite(updateinformation
, strlen(updateinformation
), 1, fpdst2
);
915 // calculate and embed MD5 digest
917 fprintf(stderr
, "Embedding MD5 digest\n");
919 unsigned long digest_md5_offset
= 0;
920 unsigned long digest_md5_length
= 0;
922 bool rv
= appimage_get_elf_section_offset_and_length(destination
, ".digest_md5", &digest_md5_offset
, &digest_md5_length
);
924 if (!rv
|| digest_md5_offset
== 0 || digest_md5_length
== 0) {
925 die("Could not find section .digest_md5 in runtime");
928 static const unsigned long section_size
= 16;
930 if (digest_md5_length
< section_size
) {
933 ".digest_md5 section in runtime's ELF header is too small"
934 "(found %lu bytes, minimum required: %lu bytes)\n",
935 digest_md5_length
, section_size
940 char digest_buffer
[section_size
];
942 if (!appimage_type2_digest_md5(destination
, digest_buffer
)) {
943 die("Failed to calculate MD5 digest");
946 FILE* destinationfp
= fopen(destination
, "r+");
948 if (destinationfp
== NULL
) {
949 die("Failed to open AppImage for updating");
952 if (fseek(destinationfp
, digest_md5_offset
, SEEK_SET
) != 0) {
953 fclose(destinationfp
);
954 die("Failed to embed MD5 digest: could not seek to section offset");
957 if (fwrite(digest_buffer
, sizeof(char), section_size
, destinationfp
) != section_size
) {
958 fclose(destinationfp
);
959 die("Failed to embed MD5 digest: write failed");
962 fclose(destinationfp
);
966 bool using_gpg
= FALSE
;
967 bool using_shasum
= FALSE
;
969 /* The user has indicated that he wants to sign */
970 gchar
* gpg2_path
= g_find_program_in_path("gpg2");
973 gpg2_path
= g_find_program_in_path("gpg");
977 gchar
* sha256sum_path
= g_find_program_in_path("sha256sum");
979 if (!sha256sum_path
) {
980 sha256sum_path
= g_find_program_in_path("shasum");
985 fprintf(stderr
, "gpg2 or gpg is not installed, cannot sign\n");
986 } else if (!sha256sum_path
) {
987 fprintf(stderr
, "sha256sum or shasum is not installed, cannot sign\n");
989 fprintf(stderr
, "%s and %s are installed and user requested to sign, "
990 "hence signing\n", using_gpg
? "gpg" : "gpg2",
991 using_shasum
? "shasum" : "sha256sum");
994 digestfile
= br_strcat(destination
, ".digest");
997 ascfile
= br_strcat(destination
, ".digest.asc");
999 if (g_file_test(digestfile
, G_FILE_TEST_IS_REGULAR
))
1003 sprintf(command
, "%s -a256 %s", sha256sum_path
, destination
);
1005 sprintf(command
, "%s %s", sha256sum_path
, destination
);
1008 fprintf(stderr
, "%s\n", command
);
1010 fp
= popen(command
, "r");
1013 die(using_shasum
? "shasum command did not succeed" : "sha256sum command did not succeed");
1017 fgets(output
, sizeof(output
) - 1, fp
);
1020 printf("%s: %s\n", using_shasum
? "shasum" : "sha256sum",
1021 g_strsplit_set(output
, " ", -1)[0]);
1023 FILE* fpx
= fopen(digestfile
, "w");
1026 fputs(g_strsplit_set(output
, " ", -1)[0], fpx
);
1030 if (pclose(fp
) != 0)
1031 die(using_shasum
? "shasum command did not succeed" : "sha256sum command did not succeed");
1035 if (g_file_test(ascfile
, G_FILE_TEST_IS_REGULAR
))
1038 char* key_arg
= NULL
;
1040 if (sign_key
&& strlen(sign_key
) > 0) {
1041 key_arg
= calloc(sizeof(char), strlen(sign_key
) + strlen("--local-user ''"));
1043 if (key_arg
== NULL
)
1044 die("malloc() failed");
1046 strcpy(key_arg
, "--local-user '");
1047 strcat(key_arg
, sign_key
);
1048 strcat(key_arg
, "'");
1052 "%s --batch --detach-sign --armor %s %s %s",
1053 gpg2_path
, key_arg
? key_arg
: "", sign_args
? sign_args
: "", digestfile
1060 fprintf(stderr
, "%s\n", command
);
1062 fp
= popen(command
, "r");
1064 if (pclose(fp
) != 0) {
1065 fprintf(stderr
, "ERROR: %s command did not succeed, could not sign, continuing\n", using_gpg
? "gpg" : "gpg2");
1071 FILE* destinationfp
= fopen(destination
, "r+");
1073 // calculate signature
1075 unsigned long sig_offset
= 0;
1076 unsigned long sig_length
= 0;
1078 bool rv
= appimage_get_elf_section_offset_and_length(destination
, ".sha256_sig", &sig_offset
, &sig_length
);
1080 if (!rv
|| sig_offset
== 0 || sig_length
== 0) {
1081 die("Could not find section .sha256_sig in runtime");
1085 printf("sig_offset: %lu\n", sig_offset
);
1086 printf("sig_length: %lu\n", sig_length
);
1089 if (sig_offset
== 0) {
1090 die("Could not determine offset for signature");
1093 if (destinationfp
== NULL
)
1094 die("Not able to open the destination file for writing, aborting");
1096 // if(strlen(updateinformation)>sig_length)
1097 // die("signature does not fit into segment, aborting");
1099 fseek(destinationfp
, sig_offset
, SEEK_SET
);
1101 FILE* ascfilefp
= fopen(ascfile
, "rb");
1103 if (ascfilefp
== NULL
) {
1104 die("Not able to open the asc file for reading, aborting");
1107 static const int bufsize
= 1024;
1108 char buffer
[bufsize
];
1110 size_t totalBytesRead
= 0;
1112 while (!feof(ascfilefp
)) {
1113 size_t bytesRead
= fread(buffer
, sizeof(char), bufsize
, ascfilefp
);
1114 totalBytesRead
+= bytesRead
;
1116 if (totalBytesRead
> sig_length
) {
1117 die("Error: cannot embed key in AppImage: size exceeds reserved ELF section size");
1120 size_t bytesWritten
= fwrite(buffer
, sizeof(char), bytesRead
, destinationfp
);
1122 if (bytesRead
!= bytesWritten
) {
1124 sprintf(message
, "Bytes read and written differ: %lu != %lu", (long unsigned) bytesRead
,
1125 (long unsigned) bytesWritten
);
1131 if (g_file_test(digestfile
, G_FILE_TEST_IS_REGULAR
))
1134 if (sign_key
== NULL
|| strlen(sign_key
) > 0) {
1135 // read which key was used to sign from signature
1136 sprintf(command
, "%s --batch --list-packets %s", gpg2_path
, ascfile
);
1138 fp
= popen(command
, "r");
1141 die("Failed to call gpg[2] to detect signature's key ID");
1144 size_t bytesRead
= fread(buffer
, sizeof(char), bufsize
, fp
);
1146 char* keyid_pos
= strstr(buffer
, "keyid");
1148 if (keyid_pos
== NULL
)
1151 char* keyIDBegin
= keyid_pos
+ strlen("keyid ");
1152 char* endOfKeyID
= strstr(keyIDBegin
, "\n");
1154 sign_key
= calloc(endOfKeyID
- keyIDBegin
, sizeof(char));
1155 memcpy(sign_key
, keyIDBegin
, endOfKeyID
- keyIDBegin
);
1158 // read rest of process input to avoid broken pipe error
1160 fread(buffer
, sizeof(char), bufsize
, fp
);
1163 int retval
= pclose(fp
);
1167 die("Failed to call gpg[2] to detect signature's key ID");
1170 if (g_file_test(ascfile
, G_FILE_TEST_IS_REGULAR
))
1174 // export key and write into section
1176 sprintf(command
, "%s --batch --export --armor %s", gpg2_path
, sign_key
);
1178 unsigned long key_offset
= 0, key_length
= 0;
1180 bool rv
= appimage_get_elf_section_offset_and_length(destination
, ".sig_key", &key_offset
, &key_length
);
1183 printf("key_offset: %lu\n", key_offset
);
1184 printf("key_length: %lu\n", key_length
);
1187 if (!rv
|| key_offset
== 0 || key_length
== 0) {
1188 die("Could not find section .sig_key in runtime");
1191 fseek(destinationfp
, key_offset
, SEEK_SET
);
1193 fp
= popen(command
, "r");
1196 die("Failed to call gpg[2] to export the signature key");
1198 static const int bufsize
= 1024;
1199 char buffer
[bufsize
];
1201 size_t totalBytesRead
= 0;
1203 size_t bytesRead
= fread(buffer
, sizeof(char), bufsize
, fp
);
1204 totalBytesRead
+= bytesRead
;
1206 if (totalBytesRead
> key_length
) {
1207 // read rest of process input to avoid broken pipe error
1209 fread(buffer
, sizeof(char), bufsize
, fp
);
1213 die("Error: cannot embed key in AppImage: size exceeds reserved ELF section size");
1216 size_t bytesWritten
= fwrite(buffer
, sizeof(char), bytesRead
, destinationfp
);
1218 if (bytesRead
!= bytesWritten
) {
1220 sprintf(message
, "Error: Bytes read and written differ: %lu != %lu",
1221 (long unsigned) bytesRead
, (long unsigned) bytesWritten
);
1226 int exportexitcode
= pclose(fp
);
1229 if (exportexitcode
!= 0) {
1231 sprintf(message
, "GPG key export failed: exit code %d", exportexitcode
);
1235 fclose(destinationfp
);
1240 /* If updateinformation was provided, then we also generate the zsync file (after having signed the AppImage) */
1241 if (updateinformation
!= NULL
) {
1242 gchar
* zsyncmake_path
= g_find_program_in_path("zsyncmake");
1243 if (!zsyncmake_path
) {
1244 fprintf(stderr
, "zsyncmake is not installed/bundled, skipping\n");
1246 fprintf(stderr
, "zsyncmake is available and updateinformation is provided, "
1247 "hence generating zsync file\n");
1252 fprintf(stderr
, "%s %s -u %s", zsyncmake_path
, destination
, basename(destination
));
1256 die("fork() failed");
1257 } else if (pid
== 0) {
1258 char* zsyncmake_command
[] = {zsyncmake_path
, destination
, "-u", basename(destination
), NULL
};
1260 // redirect stdout/stderr to /dev/null
1261 int fd
= open("/dev/null", O_WRONLY
);
1267 execv(zsyncmake_path
, zsyncmake_command
);
1269 die("execv() should not return");
1273 if (waitpid(pid
, &exitstatus
, 0) == -1) {
1274 perror("waitpid() failed");
1278 if (WEXITSTATUS(exitstatus
) != 0)
1279 die("zsyncmake command did not succeed");
1284 fprintf(stderr
, "Success\n\n");
1285 fprintf(stderr
, "Please consider submitting your AppImage to AppImageHub, the crowd-sourced\n");
1286 fprintf(stderr
, "central directory of available AppImages, by opening a pull request\n");
1287 fprintf(stderr
, "at https://github.com/AppImage/appimage.github.io\n");
1290 /* If the first argument is a regular file, then we assume that we should unpack it */
1291 if (g_file_test (remaining_args
[0], G_FILE_TEST_IS_REGULAR
)){
1292 fprintf (stdout
, "%s is a file, assuming it is an AppImage and should be unpacked\n", remaining_args
[0]);
1293 die("To be implemented");