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>
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
])
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;
315 fprintf(stderr
, "%s used for determining architecture i386\n", sourcename
);
318 if (e_machine
== 62) {
319 archs
[fARCH_x86_64
] = 1;
321 fprintf(stderr
, "%s used for determining architecture x86_64\n", sourcename
);
324 if (e_machine
== 40) {
325 archs
[fARCH_arm
] = 1;
327 fprintf(stderr
, "%s used for determining architecture armhf\n", sourcename
);
330 if (e_machine
== 183) {
331 archs
[fARCH_aarch64
] = 1;
333 fprintf(stderr
, "%s used for determining architecture aarch64\n", sourcename
);
337 void extract_arch_from_text(gchar
*archname
, const gchar
* sourcename
, bool* archs
) {
339 archname
= g_strstrip(archname
);
341 replacestr(archname
, "-", "_");
342 replacestr(archname
, " ", "_");
343 if (g_ascii_strncasecmp("i386", archname
, 20) == 0
344 || g_ascii_strncasecmp("i486", archname
, 20) == 0
345 || g_ascii_strncasecmp("i586", archname
, 20) == 0
346 || g_ascii_strncasecmp("i686", archname
, 20) == 0
347 || g_ascii_strncasecmp("intel_80386", archname
, 20) == 0
348 || g_ascii_strncasecmp("intel_80486", archname
, 20) == 0
349 || g_ascii_strncasecmp("intel_80586", archname
, 20) == 0
350 || g_ascii_strncasecmp("intel_80686", archname
, 20) == 0
352 archs
[fARCH_i386
] = 1;
354 fprintf(stderr
, "%s used for determining architecture i386\n", sourcename
);
355 } else if (g_ascii_strncasecmp("x86_64", archname
, 20) == 0) {
356 archs
[fARCH_x86_64
] = 1;
358 fprintf(stderr
, "%s used for determining architecture x86_64\n", sourcename
);
359 } else if (g_ascii_strncasecmp("arm", archname
, 20) == 0) {
360 archs
[fARCH_arm
] = 1;
362 fprintf(stderr
, "%s used for determining architecture ARM\n", sourcename
);
363 } else if (g_ascii_strncasecmp("arm_aarch64", archname
, 20) == 0) {
364 archs
[fARCH_aarch64
] = 1;
366 fprintf(stderr
, "%s used for determining architecture ARM aarch64\n", sourcename
);
372 int16_t read_elf_e_machine_field(const gchar
* file_path
) {
373 int16_t e_machine
= 0x00;
375 file
= fopen(file_path
, "rb");
377 fseek(file
, 0x12, SEEK_SET
);
378 fgets((char*) (&e_machine
), 0x02, file
);
385 void guess_arch_of_file(const gchar
*archfile
, bool* archs
) {
386 int16_t e_machine_field
= read_elf_e_machine_field(archfile
);
387 extract_arch_from_e_machine_field(e_machine_field
, archfile
, archs
);
390 void find_arch(const gchar
*real_path
, const gchar
*pattern
, bool* archs
) {
393 dir
= g_dir_open(real_path
, 0, NULL
);
396 while ((entry
= g_dir_read_name(dir
)) != NULL
) {
397 full_name
= g_build_filename(real_path
, entry
, NULL
);
398 if (g_file_test(full_name
, G_FILE_TEST_IS_SYMLINK
)) {
399 } else if (g_file_test(full_name
, G_FILE_TEST_IS_DIR
)) {
400 find_arch(full_name
, pattern
, archs
);
401 } else if (g_file_test(g_pattern_match_simple(pattern
, entry
))) {
402 guess_arch_of_file(full_name
, archs
);
408 g_warning("%s: %s", real_path
, g_strerror(errno
));
412 gchar
* find_first_matching_file_nonrecursive(const gchar
*real_path
, const gchar
*pattern
) {
415 dir
= g_dir_open(real_path
, 0, NULL
);
418 while ((entry
= g_dir_read_name(dir
)) != NULL
) {
419 full_name
= g_build_filename(real_path
, entry
, NULL
);
420 if (g_file_test(full_name
, G_FILE_TEST_IS_REGULAR
)) {
421 if(g_pattern_match_simple(pattern
, entry
))
428 g_warning("%s: %s", real_path
, g_strerror(errno
));
433 gchar
* get_desktop_entry(GKeyFile
*kf
, char *key
) {
434 gchar
*value
= g_key_file_get_string (kf
, "Desktop Entry", key
, NULL
);
436 fprintf(stderr
, "%s entry not found in desktop file\n", key
);
441 bool readFile(char* filename
, int* size
, char** buffer
) {
442 FILE* f
= fopen(filename
, "rb");
449 fseek(f
, 0, SEEK_END
);
450 long fsize
= ftell(f
);
451 fseek(f
, 0, SEEK_SET
);
453 char *indata
= malloc(fsize
);
454 fread(indata
, fsize
, 1, f
);
461 // #####################################################################
463 static GOptionEntry entries
[] =
465 { "list", 'l', 0, G_OPTION_ARG_NONE
, &list
, "List files in SOURCE AppImage", NULL
},
466 { "updateinformation", 'u', 0, G_OPTION_ARG_STRING
, &updateinformation
, "Embed update information STRING; if zsyncmake is installed, generate zsync file", NULL
},
467 { "guess", 'g', 0, G_OPTION_ARG_NONE
, &guessupdateinformation
, "Guess update information based on Travis CI or GitLab environment variables", NULL
},
468 { "bintray-user", 0, 0, G_OPTION_ARG_STRING
, &bintray_user
, "Bintray user name", NULL
},
469 { "bintray-repo", 0, 0, G_OPTION_ARG_STRING
, &bintray_repo
, "Bintray repository", NULL
},
470 { "version", 0, 0, G_OPTION_ARG_NONE
, &showVersionOnly
, "Show version number", NULL
},
471 { "verbose", 'v', 0, G_OPTION_ARG_NONE
, &verbose
, "Produce verbose output", NULL
},
472 { "sign", 's', 0, G_OPTION_ARG_NONE
, &sign
, "Sign with gpg[2]", NULL
},
473 { "comp", 0, 0, G_OPTION_ARG_STRING
, &sqfs_comp
, "Squashfs compression", NULL
},
474 { "no-appstream", 'n', 0, G_OPTION_ARG_NONE
, &no_appstream
, "Do not check AppStream metadata", NULL
},
475 { "exclude-file", 0, 0, G_OPTION_ARG_STRING
, &exclude_file
, _exclude_file_desc
, NULL
},
476 { "runtime-file", 0, 0, G_OPTION_ARG_STRING
, &runtime_file
, "Runtime file to use", NULL
},
477 { "sign-key", 0, 0, G_OPTION_ARG_STRING
, &sign_key
, "Key ID to use for gpg[2] signatures", NULL
},
478 { "sign-args", 0, 0, G_OPTION_ARG_STRING
, &sign_args
, "Extra arguments to use when signing with gpg[2]", NULL
},
479 { G_OPTION_REMAINING
, 0, 0, G_OPTION_ARG_FILENAME_ARRAY
, &remaining_args
, NULL
, NULL
},
484 main (int argc
, char *argv
[])
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 /* Parse VERSION environment variable.
556 * We cannot use g_environ_getenv (g_get_environ() since it is too new for CentOS 6
557 * Also, if VERSION is not set and -g is called and if git is on the path, use
558 * git rev-parse --short HEAD
559 * TODO: Might also want to somehow make use of
560 * git rev-parse --abbrev-ref HEAD
561 * git log -1 --format=%ci */
562 gchar
*version_env
; // In which cases do we need to malloc() here?
563 version_env
= getenv("VERSION");
564 if(guessupdateinformation
){
565 if(g_find_program_in_path ("git")) {
566 if (version_env
== NULL
) {
567 GError
*error
= NULL
;
569 GString
*command_line
= g_string_new("git");
570 g_string_append_printf(command_line
, " rev-parse --short HEAD");
571 int ret
= g_spawn_command_line_sync(command_line
->str
, &out
, NULL
, NULL
, &error
);
572 g_assert_no_error(error
);
574 version_env
= g_strstrip(out
);
576 g_print("Failed to run 'git rev-parse --short HEAD'");
578 g_string_free(command_line
, true);
579 if (version_env
!= NULL
) {
580 g_print("NOTE: Using the output of 'git rev-parse --short HEAD' as the version:\n");
581 g_print(" %s\n", version_env
);
582 g_print(" Please set the $VERSION environment variable if this is not intended\n");
588 if(!((0 == strcmp(sqfs_comp
, "gzip")) || (0 ==strcmp(sqfs_comp
, "xz"))))
589 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.");
590 /* Check for dependencies here. Better fail early if they are not present. */
591 if(! g_find_program_in_path ("file"))
592 die("file command is missing but required, please install it");
593 #ifndef AUXILIARY_FILES_DESTINATION
594 if(! g_find_program_in_path ("mksquashfs"))
595 die("mksquashfs command is missing but required, please install it");
598 // build path relative to appimagetool binary
599 char *appimagetoolDirectory
= dirname(realpath("/proc/self/exe", NULL
));
600 if (!appimagetoolDirectory
) {
601 g_print("Could not access /proc/self/exe\n");
605 pathToMksquashfs
= g_build_filename(appimagetoolDirectory
, "..", AUXILIARY_FILES_DESTINATION
, "mksquashfs", NULL
);
607 if (!g_file_test(pathToMksquashfs
, G_FILE_TEST_EXISTS
| G_FILE_TEST_IS_EXECUTABLE
)) {
608 g_printf("No such file or directory: %s\n", pathToMksquashfs
);
609 g_free(pathToMksquashfs
);
614 if(! g_find_program_in_path ("desktop-file-validate"))
615 die("desktop-file-validate command is missing, please install it");
616 if(! g_find_program_in_path ("zsyncmake"))
617 g_print("WARNING: zsyncmake command is missing, please install it if you want to use binary delta updates\n");
619 if(! g_find_program_in_path ("appstreamcli"))
620 g_print("WARNING: appstreamcli command is missing, please install it if you want to use AppStream metadata\n");
621 if(! g_find_program_in_path ("gpg2") && ! g_find_program_in_path ("gpg"))
622 g_print("WARNING: gpg2 or gpg command is missing, please install it if you want to create digital signatures\n");
623 if(! g_find_program_in_path ("sha256sum") && ! g_find_program_in_path ("shasum"))
624 g_print("WARNING: sha256sum or shasum command is missing, please install it if you want to create digital signatures\n");
626 if(!&remaining_args
[0])
627 die("SOURCE is missing");
629 /* If in list mode */
631 sfs_ls(remaining_args
[0]);
635 /* If the first argument is a directory, then we assume that we should package it */
636 if (g_file_test (remaining_args
[0], G_FILE_TEST_IS_DIR
)){
638 char source
[PATH_MAX
];
639 realpath(remaining_args
[0], source
);
641 /* Check if *.desktop file is present in source AppDir */
642 gchar
*desktop_file
= find_first_matching_file_nonrecursive(source
, "*.desktop");
643 if(desktop_file
== NULL
){
644 die("Desktop file not found, aborting");
647 fprintf (stdout
, "Desktop file: %s\n", desktop_file
);
649 if(g_find_program_in_path ("desktop-file-validate")) {
650 if(validate_desktop_file(desktop_file
) != 0){
651 fprintf(stderr
, "ERROR: Desktop file contains errors. Please fix them. Please see\n");
652 fprintf(stderr
, " https://standards.freedesktop.org/desktop-entry-spec/1.0/n");
653 die(" for more information.");
657 /* Read information from .desktop file */
658 GKeyFile
*kf
= g_key_file_new ();
659 if (!g_key_file_load_from_file (kf
, desktop_file
, G_KEY_FILE_KEEP_TRANSLATIONS
| G_KEY_FILE_KEEP_COMMENTS
, NULL
))
660 die(".desktop file cannot be parsed");
663 fprintf (stderr
,"Name: %s\n", get_desktop_entry(kf
, "Name"));
664 fprintf (stderr
,"Icon: %s\n", get_desktop_entry(kf
, "Icon"));
665 fprintf (stderr
,"Exec: %s\n", get_desktop_entry(kf
, "Exec"));
666 fprintf (stderr
,"Comment: %s\n", get_desktop_entry(kf
, "Comment"));
667 fprintf (stderr
,"Type: %s\n", get_desktop_entry(kf
, "Type"));
668 fprintf (stderr
,"Categories: %s\n", get_desktop_entry(kf
, "Categories"));
671 /* Determine the architecture */
672 bool archs
[4] = {0, 0, 0, 0};
673 extract_arch_from_text(getenv("ARCH"), "Environmental variable ARCH", archs
);
674 if (count_archs(archs
) != 1) {
675 /* If no $ARCH variable is set check a file */
676 /* We use the next best .so that we can find to determine the architecture */
677 find_arch(source
, "*.so.*", archs
);
678 int countArchs
= count_archs(archs
);
679 if (countArchs
!= 1) {
681 fprintf(stderr
, "Unable to guess the architecture of the AppDir source directory \"%s\"\n", remaining_args
[0]);
683 fprintf(stderr
, "More than one architectures were found of the AppDir source directory \"%s\"\n", remaining_args
[0]);
684 fprintf(stderr
, "A valid architecture with the ARCH environmental variable should be provided\ne.g. ARCH=x86_64 %s", argv
[0]),
688 gchar
* arch
= getArchName(archs
);
689 fprintf(stderr
, "Using architecture %s\n", arch
);
692 char app_name_for_filename
[PATH_MAX
];
693 sprintf(app_name_for_filename
, "%s", get_desktop_entry(kf
, "Name"));
694 replacestr(app_name_for_filename
, " ", "_");
697 fprintf (stderr
,"App name for filename: %s\n", app_name_for_filename
);
699 if (remaining_args
[1]) {
700 destination
= remaining_args
[1];
702 /* No destination has been specified, to let's construct one
703 * TODO: Find out the architecture and use a $VERSION that might be around in the env */
704 char dest_path
[PATH_MAX
];
705 sprintf(dest_path
, "%s-%s.AppImage", app_name_for_filename
, arch
);
707 if (version_env
!= NULL
) {
708 sprintf(dest_path
, "%s-%s-%s.AppImage", app_name_for_filename
, version_env
, arch
);
710 // set VERSION in desktop file and save it
711 g_key_file_set_string(kf
, G_KEY_FILE_DESKTOP_GROUP
, "X-AppImage-Version", version_env
);
713 if (!g_key_file_save_to_file(kf
, desktop_file
, NULL
)) {
714 fprintf(stderr
, "Could not save modified desktop file\n");
719 destination
= strdup(dest_path
);
720 replacestr(destination
, " ", "_");
723 fprintf (stdout
, "%s should be packaged as %s\n", source
, destination
);
724 /* Check if the Icon file is how it is expected */
725 gchar
* icon_name
= get_desktop_entry(kf
, "Icon");
726 gchar
* icon_file_path
= NULL
;
727 gchar
* icon_file_png
;
728 gchar
* icon_file_svg
;
729 gchar
* icon_file_svgz
;
730 gchar
* icon_file_xpm
;
731 icon_file_png
= g_strdup_printf("%s/%s.png", source
, icon_name
);
732 icon_file_svg
= g_strdup_printf("%s/%s.svg", source
, icon_name
);
733 icon_file_svgz
= g_strdup_printf("%s/%s.svgz", source
, icon_name
);
734 icon_file_xpm
= g_strdup_printf("%s/%s.xpm", source
, icon_name
);
735 if (g_file_test(icon_file_png
, G_FILE_TEST_IS_REGULAR
)) {
736 icon_file_path
= icon_file_png
;
737 } else if(g_file_test(icon_file_svg
, G_FILE_TEST_IS_REGULAR
)) {
738 icon_file_path
= icon_file_svg
;
739 } else if(g_file_test(icon_file_svgz
, G_FILE_TEST_IS_REGULAR
)) {
740 icon_file_path
= icon_file_svgz
;
741 } else if(g_file_test(icon_file_xpm
, G_FILE_TEST_IS_REGULAR
)) {
742 icon_file_path
= icon_file_xpm
;
744 fprintf (stderr
, "%s{.png,.svg,.svgz,.xpm} defined in desktop file but not found\n", icon_name
);
745 fprintf (stderr
, "For example, you could put a 256x256 pixel png into\n");
746 gchar
*icon_name_with_png
= g_strconcat(icon_name
, ".png", NULL
);
747 gchar
*example_path
= g_build_filename(source
, "/", icon_name_with_png
, NULL
);
748 fprintf (stderr
, "%s\n", example_path
);
752 /* Check if .DirIcon is present in source AppDir */
753 gchar
*diricon_path
= g_build_filename(source
, ".DirIcon", NULL
);
755 if (! g_file_test(diricon_path
, G_FILE_TEST_EXISTS
)){
756 fprintf (stderr
, "Deleting pre-existing .DirIcon\n");
757 g_unlink(diricon_path
);
759 if (! g_file_test(diricon_path
, G_FILE_TEST_IS_REGULAR
)){
760 fprintf (stderr
, "Creating .DirIcon symlink based on information from desktop file\n");
761 int res
= symlink(basename(icon_file_path
), diricon_path
);
763 die("Could not symlink .DirIcon");
766 /* Check if AppStream upstream metadata is present in source AppDir */
768 char application_id
[PATH_MAX
];
769 sprintf (application_id
, "%s", basename(desktop_file
));
770 replacestr(application_id
, ".desktop", ".appdata.xml");
771 gchar
*appdata_path
= g_build_filename(source
, "/usr/share/metainfo/", application_id
, NULL
);
772 if (! g_file_test(appdata_path
, G_FILE_TEST_IS_REGULAR
)){
773 fprintf (stderr
, "WARNING: AppStream upstream metadata is missing, please consider creating it\n");
774 fprintf (stderr
, " in usr/share/metainfo/%s\n", application_id
);
775 fprintf (stderr
, " Please see https://www.freedesktop.org/software/appstream/docs/chap-Quickstart.html#sect-Quickstart-DesktopApps\n");
776 fprintf (stderr
, " for more information or use the generator at http://output.jsbin.com/qoqukof.\n");
778 fprintf (stderr
, "AppStream upstream metadata found in usr/share/metainfo/%s\n", application_id
);
779 /* Use ximion's appstreamcli to make sure that desktop file and appdata match together */
780 if(g_find_program_in_path ("appstreamcli")) {
781 sprintf (command
, "%s validate-tree %s", g_find_program_in_path ("appstreamcli"), source
);
782 g_print("Trying to validate AppStream information with the appstreamcli tool\n");
783 g_print("In case of issues, please refer to https://github.com/ximion/appstream\n");
784 int ret
= system(command
);
786 die("Failed to validate AppStream information with appstreamcli");
788 /* It seems that hughsie's appstream-util does additional validations */
789 if(g_find_program_in_path ("appstream-util")) {
790 sprintf (command
, "%s validate-relax %s", g_find_program_in_path ("appstream-util"), appdata_path
);
791 g_print("Trying to validate AppStream information with the appstream-util tool\n");
792 g_print("In case of issues, please refer to https://github.com/hughsie/appstream-glib\n");
793 int ret
= system(command
);
795 die("Failed to validate AppStream information with appstream-util");
800 /* Upstream mksquashfs can currently not start writing at an offset,
801 * so we need a patched one. https://github.com/plougher/squashfs-tools/pull/13
802 * should hopefully change that. */
804 fprintf (stderr
, "Generating squashfs...\n");
807 bool using_external_data
= false;
808 if (runtime_file
!= NULL
) {
809 if (!readFile(runtime_file
, &size
, &data
))
810 die("Unable to load provided runtime file");
811 using_external_data
= true;
813 #ifdef HAVE_BINARY_RUNTIME
814 /* runtime is embedded into this executable
815 * http://stupefydeveloper.blogspot.de/2008/08/cc-embed-binary-data-into-elf.html */
819 die("No runtime file was provided");
823 printf("Size of the embedded runtime: %d bytes\n", size
);
825 int result
= sfs_mksquashfs(source
, destination
, size
);
827 die("sfs_mksquashfs error");
829 fprintf (stderr
, "Embedding ELF...\n");
830 FILE *fpdst
= fopen(destination
, "rb+");
832 die("Not able to open the AppImage for writing, aborting");
835 fseek(fpdst
, 0, SEEK_SET
);
836 fwrite(data
, size
, 1, fpdst
);
838 if (using_external_data
)
841 fprintf (stderr
, "Marking the AppImage as executable...\n");
842 if (chmod (destination
, 0755) < 0) {
843 printf("Could not set executable bit, aborting\n");
847 if(bintray_user
!= NULL
){
848 if(bintray_repo
!= NULL
){
850 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
);
851 updateinformation
= buf
;
852 printf("%s\n", updateinformation
);
856 /* If the user has not provided update information but we know this is a Travis CI build,
857 * then fill in update information based on TRAVIS_REPO_SLUG */
858 if(guessupdateinformation
){
859 if(travis_repo_slug
){
861 printf("Will not guess update information since $GITHUB_TOKEN is missing,\n");
862 if(0 != strcmp(travis_pull_request
, "false")){
863 printf("please set it in the Travis CI Repository Settings for this project.\n");
864 printf("You can get one from https://github.com/settings/tokens\n");
866 printf("which is expected since this is a pull request\n");
869 gchar
*zsyncmake_path
= g_find_program_in_path ("zsyncmake");
872 gchar
**parts
= g_strsplit (travis_repo_slug
, "/", 2);
873 /* https://github.com/AppImage/AppImageSpec/blob/master/draft.md#github-releases
874 * gh-releases-zsync|probono|AppImages|latest|Subsurface*-x86_64.AppImage.zsync */
875 gchar
*channel
= "continuous";
876 if(travis_tag
!= NULL
){
877 if((strcmp(travis_tag
, "") != 0) && (strcmp(travis_tag
, "continuous") != 0)) {
881 sprintf(buf
, "gh-releases-zsync|%s|%s|%s|%s*-%s.AppImage.zsync", parts
[0], parts
[1], channel
, app_name_for_filename
, arch
);
882 updateinformation
= buf
;
883 printf("Guessing update information based on $TRAVIS_TAG=%s and $TRAVIS_REPO_SLUG=%s\n", travis_tag
, travis_repo_slug
);
884 printf("%s\n", updateinformation
);
886 printf("Will not guess update information since zsyncmake is missing\n");
889 } else if(CI_COMMIT_REF_NAME
){
890 // ${CI_PROJECT_URL}/-/jobs/artifacts/${CI_COMMIT_REF_NAME}/raw/QtQuickApp-x86_64.AppImage?job=${CI_JOB_NAME}
891 gchar
*zsyncmake_path
= g_find_program_in_path ("zsyncmake");
894 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
);
895 updateinformation
= buf
;
896 printf("Guessing update information based on $CI_COMMIT_REF_NAME=%s and $CI_JOB_NAME=%s\n", CI_COMMIT_REF_NAME
, CI_JOB_NAME
);
897 printf("%s\n", updateinformation
);
899 printf("Will not guess update information since zsyncmake is missing\n");
904 /* If updateinformation was provided, then we check and embed it */
905 if(updateinformation
!= NULL
){
906 if(!g_str_has_prefix(updateinformation
,"zsync|"))
907 if(!g_str_has_prefix(updateinformation
,"bintray-zsync|"))
908 if(!g_str_has_prefix(updateinformation
,"gh-releases-zsync|"))
909 die("The provided updateinformation is not in a recognized format");
911 gchar
**ui_type
= g_strsplit_set(updateinformation
, "|", -1);
914 printf("updateinformation type: %s\n", ui_type
[0]);
915 /* TODO: Further checking of the updateinformation */
918 unsigned long ui_offset
= 0;
919 unsigned long ui_length
= 0;
921 bool rv
= appimage_get_elf_section_offset_and_length(destination
, ".upd_info", &ui_offset
, &ui_length
);
923 if (!rv
|| ui_offset
== 0 || ui_length
== 0) {
924 die("Could not find section .upd_info in runtime");
928 printf("ui_offset: %lu\n", ui_offset
);
929 printf("ui_length: %lu\n", ui_length
);
932 die("Could not determine offset for updateinformation");
934 if(strlen(updateinformation
)>ui_length
)
935 die("updateinformation does not fit into segment, aborting");
936 FILE *fpdst2
= fopen(destination
, "r+");
938 die("Not able to open the destination file for writing, aborting");
939 fseek(fpdst2
, ui_offset
, SEEK_SET
);
940 // fseek(fpdst2, ui_offset, SEEK_SET);
941 // fwrite(0x00, 1, 1024, fpdst); // FIXME: Segfaults; why?
942 // fseek(fpdst, ui_offset, SEEK_SET);
943 fwrite(updateinformation
, strlen(updateinformation
), 1, fpdst2
);
948 // calculate and embed MD5 digest
950 fprintf(stderr
, "Embedding MD5 digest\n");
952 unsigned long digest_md5_offset
= 0;
953 unsigned long digest_md5_length
= 0;
955 bool rv
= appimage_get_elf_section_offset_and_length(destination
, ".digest_md5", &digest_md5_offset
, &digest_md5_length
);
957 if (!rv
|| digest_md5_offset
== 0 || digest_md5_length
== 0) {
958 die("Could not find section .digest_md5 in runtime");
961 static const unsigned long section_size
= 16;
963 if (digest_md5_length
< section_size
) {
966 ".digest_md5 section in runtime's ELF header is too small"
967 "(found %lu bytes, minimum required: %lu bytes)\n",
968 digest_md5_length
, section_size
973 char digest_buffer
[section_size
];
975 if (!appimage_type2_digest_md5(destination
, digest_buffer
)) {
976 die("Failed to calculate MD5 digest");
979 FILE* destinationfp
= fopen(destination
, "r+");
981 if (destinationfp
== NULL
) {
982 die("Failed to open AppImage for updating");
985 if (fseek(destinationfp
, digest_md5_offset
, SEEK_SET
) != 0) {
986 fclose(destinationfp
);
987 die("Failed to embed MD5 digest: could not seek to section offset");
990 if (fwrite(digest_buffer
, sizeof(char), section_size
, destinationfp
) != section_size
) {
991 fclose(destinationfp
);
992 die("Failed to embed MD5 digest: write failed");
995 fclose(destinationfp
);
999 bool using_gpg
= FALSE
;
1000 bool using_shasum
= FALSE
;
1002 /* The user has indicated that he wants to sign */
1003 gchar
* gpg2_path
= g_find_program_in_path("gpg2");
1006 gpg2_path
= g_find_program_in_path("gpg");
1010 gchar
* sha256sum_path
= g_find_program_in_path("sha256sum");
1012 if (!sha256sum_path
) {
1013 sha256sum_path
= g_find_program_in_path("shasum");
1018 fprintf(stderr
, "gpg2 or gpg is not installed, cannot sign\n");
1019 } else if (!sha256sum_path
) {
1020 fprintf(stderr
, "sha256sum or shasum is not installed, cannot sign\n");
1022 fprintf(stderr
, "%s and %s are installed and user requested to sign, "
1023 "hence signing\n", using_gpg
? "gpg" : "gpg2",
1024 using_shasum
? "shasum" : "sha256sum");
1027 digestfile
= br_strcat(destination
, ".digest");
1030 ascfile
= br_strcat(destination
, ".digest.asc");
1032 if (g_file_test(digestfile
, G_FILE_TEST_IS_REGULAR
))
1036 sprintf(command
, "%s -a256 %s", sha256sum_path
, destination
);
1038 sprintf(command
, "%s %s", sha256sum_path
, destination
);
1041 fprintf(stderr
, "%s\n", command
);
1043 fp
= popen(command
, "r");
1046 die(using_shasum
? "shasum command did not succeed" : "sha256sum command did not succeed");
1050 fgets(output
, sizeof(output
) - 1, fp
);
1053 printf("%s: %s\n", using_shasum
? "shasum" : "sha256sum",
1054 g_strsplit_set(output
, " ", -1)[0]);
1056 FILE* fpx
= fopen(digestfile
, "w");
1059 fputs(g_strsplit_set(output
, " ", -1)[0], fpx
);
1063 if (pclose(fp
) != 0)
1064 die(using_shasum
? "shasum command did not succeed" : "sha256sum command did not succeed");
1068 if (g_file_test(ascfile
, G_FILE_TEST_IS_REGULAR
))
1071 char* key_arg
= NULL
;
1073 if (sign_key
&& strlen(sign_key
) > 0) {
1074 key_arg
= calloc(sizeof(char), strlen(sign_key
) + strlen("--local-user ''"));
1076 if (key_arg
== NULL
)
1077 die("malloc() failed");
1079 strcpy(key_arg
, "--local-user '");
1080 strcat(key_arg
, sign_key
);
1081 strcat(key_arg
, "'");
1085 "%s --batch --detach-sign --armor %s %s %s",
1086 gpg2_path
, key_arg
? key_arg
: "", sign_args
? sign_args
: "", digestfile
1093 fprintf(stderr
, "%s\n", command
);
1095 fp
= popen(command
, "r");
1097 if (pclose(fp
) != 0) {
1098 fprintf(stderr
, "ERROR: %s command did not succeed, could not sign, continuing\n", using_gpg
? "gpg" : "gpg2");
1104 FILE* destinationfp
= fopen(destination
, "r+");
1106 // calculate signature
1108 unsigned long sig_offset
= 0;
1109 unsigned long sig_length
= 0;
1111 bool rv
= appimage_get_elf_section_offset_and_length(destination
, ".sha256_sig", &sig_offset
, &sig_length
);
1113 if (!rv
|| sig_offset
== 0 || sig_length
== 0) {
1114 die("Could not find section .sha256_sig in runtime");
1118 printf("sig_offset: %lu\n", sig_offset
);
1119 printf("sig_length: %lu\n", sig_length
);
1122 if (sig_offset
== 0) {
1123 die("Could not determine offset for signature");
1126 if (destinationfp
== NULL
)
1127 die("Not able to open the destination file for writing, aborting");
1129 // if(strlen(updateinformation)>sig_length)
1130 // die("signature does not fit into segment, aborting");
1132 fseek(destinationfp
, sig_offset
, SEEK_SET
);
1134 FILE* ascfilefp
= fopen(ascfile
, "rb");
1136 if (ascfilefp
== NULL
) {
1137 die("Not able to open the asc file for reading, aborting");
1140 static const int bufsize
= 1024;
1141 char buffer
[bufsize
];
1143 size_t totalBytesRead
= 0;
1145 while (!feof(ascfilefp
)) {
1146 size_t bytesRead
= fread(buffer
, sizeof(char), bufsize
, ascfilefp
);
1147 totalBytesRead
+= bytesRead
;
1149 if (totalBytesRead
> sig_length
) {
1150 die("Error: cannot embed key in AppImage: size exceeds reserved ELF section size");
1153 size_t bytesWritten
= fwrite(buffer
, sizeof(char), bytesRead
, destinationfp
);
1155 if (bytesRead
!= bytesWritten
) {
1157 sprintf(message
, "Bytes read and written differ: %lu != %lu", (long unsigned) bytesRead
,
1158 (long unsigned) bytesWritten
);
1164 if (g_file_test(digestfile
, G_FILE_TEST_IS_REGULAR
))
1167 if (sign_key
== NULL
|| strlen(sign_key
) > 0) {
1168 // read which key was used to sign from signature
1169 sprintf(command
, "%s --batch --list-packets %s", gpg2_path
, ascfile
);
1171 fp
= popen(command
, "r");
1174 die("Failed to call gpg[2] to detect signature's key ID");
1177 size_t bytesRead
= fread(buffer
, sizeof(char), bufsize
, fp
);
1179 char* keyid_pos
= strstr(buffer
, "keyid");
1181 if (keyid_pos
== NULL
)
1184 char* keyIDBegin
= keyid_pos
+ strlen("keyid ");
1185 char* endOfKeyID
= strstr(keyIDBegin
, "\n");
1187 sign_key
= calloc(endOfKeyID
- keyIDBegin
, sizeof(char));
1188 memcpy(sign_key
, keyIDBegin
, endOfKeyID
- keyIDBegin
);
1191 // read rest of process input to avoid broken pipe error
1193 fread(buffer
, sizeof(char), bufsize
, fp
);
1196 int retval
= pclose(fp
);
1200 die("Failed to call gpg[2] to detect signature's key ID");
1203 if (g_file_test(ascfile
, G_FILE_TEST_IS_REGULAR
))
1207 // export key and write into section
1209 sprintf(command
, "%s --batch --export --armor %s", gpg2_path
, sign_key
);
1211 unsigned long key_offset
= 0, key_length
= 0;
1213 bool rv
= appimage_get_elf_section_offset_and_length(destination
, ".sig_key", &key_offset
, &key_length
);
1216 printf("key_offset: %lu\n", key_offset
);
1217 printf("key_length: %lu\n", key_length
);
1220 if (!rv
|| key_offset
== 0 || key_length
== 0) {
1221 die("Could not find section .sig_key in runtime");
1224 fseek(destinationfp
, key_offset
, SEEK_SET
);
1226 fp
= popen(command
, "r");
1229 die("Failed to call gpg[2] to export the signature key");
1231 static const int bufsize
= 1024;
1232 char buffer
[bufsize
];
1234 size_t totalBytesRead
= 0;
1236 size_t bytesRead
= fread(buffer
, sizeof(char), bufsize
, fp
);
1237 totalBytesRead
+= bytesRead
;
1239 if (totalBytesRead
> key_length
) {
1240 // read rest of process input to avoid broken pipe error
1242 fread(buffer
, sizeof(char), bufsize
, fp
);
1246 die("Error: cannot embed key in AppImage: size exceeds reserved ELF section size");
1249 size_t bytesWritten
= fwrite(buffer
, sizeof(char), bytesRead
, destinationfp
);
1251 if (bytesRead
!= bytesWritten
) {
1253 sprintf(message
, "Error: Bytes read and written differ: %lu != %lu",
1254 (long unsigned) bytesRead
, (long unsigned) bytesWritten
);
1259 int exportexitcode
= pclose(fp
);
1262 if (exportexitcode
!= 0) {
1264 sprintf(message
, "GPG key export failed: exit code %d", exportexitcode
);
1268 fclose(destinationfp
);
1273 /* If updateinformation was provided, then we also generate the zsync file (after having signed the AppImage) */
1274 if (updateinformation
!= NULL
) {
1275 gchar
* zsyncmake_path
= g_find_program_in_path("zsyncmake");
1276 if (!zsyncmake_path
) {
1277 fprintf(stderr
, "zsyncmake is not installed/bundled, skipping\n");
1279 fprintf(stderr
, "zsyncmake is available and updateinformation is provided, "
1280 "hence generating zsync file\n");
1285 fprintf(stderr
, "%s %s -u %s", zsyncmake_path
, destination
, basename(destination
));
1289 die("fork() failed");
1290 } else if (pid
== 0) {
1291 char* zsyncmake_command
[] = {zsyncmake_path
, destination
, "-u", basename(destination
), NULL
};
1293 // redirect stdout/stderr to /dev/null
1294 int fd
= open("/dev/null", O_WRONLY
);
1300 execv(zsyncmake_path
, zsyncmake_command
);
1302 die("execv() should not return");
1306 if (waitpid(pid
, &exitstatus
, 0) == -1) {
1307 perror("waitpid() failed");
1311 if (WEXITSTATUS(exitstatus
) != 0)
1312 die("zsyncmake command did not succeed");
1317 fprintf(stderr
, "Success\n\n");
1318 fprintf(stderr
, "Please consider submitting your AppImage to AppImageHub, the crowd-sourced\n");
1319 fprintf(stderr
, "central directory of available AppImages, by opening a pull request\n");
1320 fprintf(stderr
, "at https://github.com/AppImage/appimage.github.io\n");
1323 /* If the first argument is a regular file, then we assume that we should unpack it */
1324 if (g_file_test (remaining_args
[0], G_FILE_TEST_IS_REGULAR
)){
1325 fprintf (stdout
, "%s is a file, assuming it is an AppImage and should be unpacked\n", remaining_args
[0]);
1326 die("To be implemented");