├── .gitignore ├── Makefile ├── aureate.1 ├── config.h ├── README.md └── aureate.c /.gitignore: -------------------------------------------------------------------------------- 1 | aureate -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | PREFIX = /usr/local 2 | LIBS = -lgit2 -lcurl -ljson-c 3 | FLAGS = ${LIBS} 4 | PROG = aureate 5 | 6 | all: 7 | cc ${PROG}.c ${FLAGS} -o ${PROG} 8 | 9 | debug: 10 | clang ${PROG}.c -Wall -Werror -fsanitize=undefined,address ${FLAGS} -o ${PROG} 11 | 12 | install: all 13 | install -Dm755 ./${PROG} ${PREFIX}/bin/${PROG} 14 | install -Dm644 ./${PROG}.1 ${PREFIX}/share/man/man1/${PROG}.1 15 | 16 | uninstall: 17 | rm -f ${PREFIX}/bin/${PROG} 18 | rm -f ${PREFIX}/share/man/man1/${PROG}.1 19 | 20 | clean: 21 | rm -rf ${PROG} *.out 22 | -------------------------------------------------------------------------------- /aureate.1: -------------------------------------------------------------------------------- 1 | .TH AUREATE 1 "30 March 2023" "https://swindlesmccoop.xyz" 2 | .SH NAME 3 | AUReate \- a minimalist AUR helper written in C 4 | .SH SYNOPSIS 5 | .B aureate 6 | \fI[ -S, -Ss, -R, -h ]\fP \fI[package]\fP 7 | .SH DESCRIPTION 8 | .B aureate 9 | will read the argument given and adjust its behavior accordingly. 10 | .SH ARGUMENTS 11 | .TP 12 | .IP \fI-S\fP 13 | Build \fI[package]\fP from the AUR. Can take multiple arguments. 14 | .IP \fI-Ss\fP 15 | Search the AUR for packages with names containing \fI[package]\fP. 16 | .IP \fI-h\fP 17 | Print a help message with a quick synopsis on what each option does. 18 | .IP \fI-R\fp 19 | Uninstall \fI[package]\fP. 20 | .SH AUTHORS 21 | \fIaureate\fP was written by Swindles McCoop.\fP 22 | -------------------------------------------------------------------------------- /config.h: -------------------------------------------------------------------------------- 1 | #define COLORS //comment this line to disable color output 2 | #define EDITOR "vim" //set editor for -e 3 | #define SUDO "sudo" //set privilege escalation program here 4 | 5 | #ifdef COLORS 6 | #define RED "\033[0;31m" 7 | #define GREEN "\033[0;32m" 8 | #define BLUE "\033[0;34m" 9 | #define BOLDRED "\033[1;31m" 10 | #define BOLDGREEN "\033[1;32m" 11 | #define BOLDBLUE "\033[1;34m" 12 | #define BOLDCYAN "\033[1;36m" 13 | #define BOLDWHITE "\033[1;37m" 14 | #define RESET "\033[0m" 15 | #else 16 | #define RED "\033[0m" 17 | #define GREEN "\033[0m" 18 | #define BLUE "\033[0m" 19 | #define BOLDRED "\033[0m" 20 | #define BOLDGREEN "\033[0m" 21 | #define BOLDBLUE "\033[0m" 22 | #define BOLDCYAN "\033[0m" 23 | #define BOLDWHITE "\033[0m" 24 | #define RESET "\033[0m" 25 | #endif 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Dependencies: 2 | - `libcurl` 3 | - `libgit2` 4 | - `libjson-c` 5 | 6 | ## How To Install 7 | ``` 8 | make 9 | sudo make install 10 | ``` 11 | 12 | ## How To Use 13 | Either run `man aureate` or `aureate --help` 14 | 15 | ## To Do 16 | - [x] Handle multiple packages to install at at time 17 | - [x] Add `-R` flag 18 | - [x] Parse package info with `-Ss` 19 | - [ ] Clean up parse code 20 | - [x] Reimplement `strlen()` inside of `char` combined with `snprintf()` all using `asprintf()` 21 | - [x] Replace `system()` command with `exec()` family of functions 22 | - [x] Fix `flags` function to use `getopt()` 23 | - [x] Use libgit to pull from the AUR git repos instead of redownloading tarball every time 24 | - [x] Properly wrap lines of `search()` output 25 | - [ ] `-Syu` function to update all packages 26 | - Compare clone to master with `.git/refs/heads/master` 27 | - [x] Make formatting consistent across all code 28 | - [x] Add `-e` flag to edit PKGBUILD before install -------------------------------------------------------------------------------- /aureate.c: -------------------------------------------------------------------------------- 1 | #include "config.h" 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | 17 | #define BASE_URL "https://aur.archlinux.org/" 18 | //#define PARSE_URL BASE_URL "rpc/?v=5&type=info&arg=" 19 | #define SEARCH_URL BASE_URL "rpc/?v=5&type=search&arg=%s" 20 | #define SUFFIX ".git" 21 | #define URL_MAX 256 22 | #ifndef PATH_MAX 23 | #define PATH_MAX 256 24 | #endif /* PATH_MAX */ 25 | 26 | //for API parsing 27 | struct MemoryStruct { 28 | char *memory; 29 | size_t size; 30 | }; 31 | 32 | static size_t WriteMemoryCallback(void *contents, size_t size, size_t nmemb, void *userp); 33 | static void pretty_print(const char *str); 34 | static void search(const char *pkg); 35 | static int download(int argc, char *argv[], int install); 36 | static void help(const char *progname); 37 | 38 | size_t WriteMemoryCallback(void *contents, size_t size, size_t nmemb, void *userp) { 39 | if (size != 0 && nmemb > -1 / size) 40 | errx(1, "realloc: %s", strerror(ENOMEM)); 41 | 42 | size_t realsize = size * nmemb; 43 | struct MemoryStruct *mem = (struct MemoryStruct *)userp; 44 | mem->memory = realloc(mem->memory, mem->size + realsize + 1); 45 | if (mem->memory == NULL) 46 | err(1, "realloc"); 47 | memcpy(&(mem->memory[mem->size]), contents, realsize); 48 | mem->size += realsize; 49 | mem->memory[mem->size] = '\0'; 50 | 51 | return realsize; 52 | } 53 | 54 | void pretty_print(const char *str) { 55 | struct winsize w; 56 | ioctl(STDOUT_FILENO, TIOCGWINSZ, &w); 57 | 58 | int term_width = w.ws_col; 59 | int indent = 4; 60 | int wrap_width = term_width - indent; 61 | 62 | int cur_width = indent; 63 | int start_idx = 0; 64 | int wrapped = 0; 65 | 66 | //add the initial 4 spaces 67 | printf(" "); 68 | 69 | for (int i = 0; i < (int)strlen(str); ++i) { 70 | if (str[i] == ' ' && cur_width >= wrap_width) { 71 | printf("%.*s", i - start_idx, &str[start_idx]); 72 | start_idx = i + 1; 73 | cur_width = indent; 74 | wrapped = 1; 75 | //add 4 spaces for wrapped lines 76 | printf("\n "); 77 | } else { 78 | //set wrapped to 0 for non-wrapped lines 79 | wrapped = 0; 80 | } 81 | ++cur_width; 82 | } 83 | 84 | //only print extra 4 spaces if wrapped 85 | if (wrapped) 86 | printf(" %s\n", &str[start_idx]); 87 | else 88 | printf("%s\n", &str[start_idx]); 89 | } 90 | 91 | void search(const char *pkg) { 92 | CURL *curl; 93 | CURLcode res; 94 | char url[URL_MAX]; 95 | struct MemoryStruct chunk = { 96 | .memory = NULL, 97 | .size = 0 98 | }; 99 | 100 | curl_global_init(CURL_GLOBAL_DEFAULT); 101 | curl = curl_easy_init(); 102 | 103 | if (curl == NULL) 104 | errx(1, "curl_easy_init: failed"); 105 | 106 | snprintf(url, URL_MAX, SEARCH_URL, pkg); 107 | curl_easy_setopt(curl, CURLOPT_URL, url); 108 | curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, WriteMemoryCallback); 109 | curl_easy_setopt(curl, CURLOPT_WRITEDATA, &chunk); 110 | 111 | res = curl_easy_perform(curl); 112 | if (res != CURLE_OK) 113 | errx(1, "curl_easy_perform() failed: %s", curl_easy_strerror(res)); 114 | 115 | json_object *parsed_json, *results; 116 | parsed_json = json_tokener_parse(chunk.memory); 117 | json_object_object_get_ex(parsed_json, "results", &results); 118 | 119 | int n_results = json_object_array_length(results); 120 | 121 | for (int i = 0; i < n_results; i++) { 122 | json_object *pkg_obj = json_object_array_get_idx(results, i); 123 | json_object *pkg_name, *pkg_desc, *pkg_ver; 124 | 125 | json_object_object_get_ex(pkg_obj, "Name", &pkg_name); 126 | json_object_object_get_ex(pkg_obj, "Description", &pkg_desc); 127 | json_object_object_get_ex(pkg_obj, "Version", &pkg_ver); 128 | 129 | printf(BOLDCYAN "aur" RESET "/" BOLDWHITE "%s" BOLDGREEN " %s" RESET "\n", 130 | json_object_get_string(pkg_name), json_object_get_string(pkg_ver)); 131 | 132 | //printf(" %s\n", json_object_get_string(pkg_desc)); 133 | //printf(" %s\n"); 134 | 135 | const char *desc_str = json_object_get_string(pkg_desc); 136 | pretty_print(desc_str); 137 | } 138 | json_object_put(parsed_json); 139 | 140 | curl_easy_cleanup(curl); 141 | free(chunk.memory); 142 | curl_global_cleanup(); 143 | } 144 | 145 | /* Get path for cache. Also use $HOME var */ 146 | char *cache_path(const char *env_name) 147 | { 148 | char *syscache = getenv(env_name); // ? getenv(env_name) : ".cache"; 149 | if (syscache == NULL) { 150 | char *home_dir = getenv("HOME"); 151 | static char full_path[PATH_MAX]; 152 | 153 | snprintf(full_path, PATH_MAX, "%s/.cache", home_dir); 154 | 155 | return full_path; 156 | } 157 | return syscache; 158 | } 159 | 160 | int download(int argc, char *argv[], int install) { 161 | //define vars 162 | char* syscache = cache_path("XDG_CACHE_HOME"); 163 | //construct makepkg options 164 | char makepkg_opts[4]; 165 | snprintf(makepkg_opts, 4, "-s%s", install == 1 ? "i" : ""); 166 | 167 | for (int i = 2; i < argc; i++) { 168 | const char* pkg = argv[i]; 169 | 170 | //check if XDG_CACHE_HOME exists - if not, create it 171 | struct stat st; 172 | if (stat(syscache, &st) == -1) { 173 | mkdir(syscache, 0700); 174 | } 175 | 176 | //construct aureate cache var 177 | char cache[PATH_MAX]; 178 | snprintf(cache, PATH_MAX, "%s/aureate", syscache); 179 | 180 | 181 | //do the same thing as syscache with aureate cache 182 | if (stat(cache, &st) == -1) { 183 | mkdir(cache, 0700); 184 | } 185 | 186 | //construct clone URL 187 | char clone_url[URL_MAX]; 188 | snprintf(clone_url, URL_MAX, "%s%s%s", BASE_URL, pkg, SUFFIX); 189 | 190 | //clone or pull the repo 191 | chdir(cache); 192 | git_libgit2_init(); 193 | 194 | //if directory for package already exists, clone 195 | if (stat(pkg, &st) == -1) { 196 | printf(BLUE ":: " RESET "Fetching repo..."); 197 | fflush(stdout); 198 | git_repository *repo = NULL; 199 | git_clone(&repo, clone_url, pkg, NULL); 200 | chdir(pkg); 201 | if (access("PKGBUILD", F_OK) != -1) { 202 | printf("done.\n"); fflush(stdout); 203 | } else { 204 | fprintf(stderr, "\n%s: " RED "Error: " RESET 205 | "Package name invalid or you are not" 206 | "connected to the internet.\n", argv[0]); 207 | fflush(stdout); 208 | //remove failed clone 209 | return 1; 210 | } 211 | //else, pull latest changes 212 | } else { 213 | printf(BLUE ":: " RESET "Fetching latest changes..."); 214 | fflush(stdout); 215 | git_repository *repo; 216 | git_remote *remote; 217 | 218 | git_repository_open(&repo, pkg); 219 | git_remote_lookup(&remote, repo, "origin"); 220 | git_remote_fetch(remote, NULL, NULL, "pull"); 221 | git_remote_free(remote); 222 | git_repository_free(repo); 223 | 224 | printf("done.\n"); 225 | fflush(stdout); 226 | chdir(pkg); 227 | } 228 | 229 | //handle -e 230 | if (i + 1 < argc && strcmp(argv[i + 1], "-e") == 0) { 231 | //skip -e as an arg so download() doesn't try to run it as a pkg 232 | char cmd[sizeof(EDITOR) + PATH_MAX + 1]; 233 | i++; 234 | snprintf(cmd, sizeof(EDITOR) + PATH_MAX, 235 | "%s %s/aureate/%s/PKGBUILD", 236 | EDITOR, syscache, pkg); 237 | system(cmd); 238 | } 239 | 240 | //hand off the rest to pacman 241 | execlp("makepkg", "makepkg", makepkg_opts, NULL); 242 | } 243 | 244 | return 0; 245 | } 246 | 247 | void help(const char *progname) { 248 | printf(BLUE "AUReate" RESET ": AUR helper in the C programming language\n" 249 | "Usage: %s [arguments] \n\n" 250 | "Arguments:\n" 251 | GREEN " -S: " RESET "Sync package from remote respository\n" 252 | GREEN " -Ss: " RESET "Search for package in remote respository\n" 253 | GREEN " -Sc: " RESET "Clean local cache\n" 254 | GREEN " -b: " RESET "Only build package\n" 255 | GREEN " -R: " RESET "Remove local package\n" 256 | GREEN " -h, --help: " RESET "Print this help message\n" 257 | RESET "\nYou can find more information by running " BLUE "man aureate" RESET ".\n", progname); 258 | } 259 | 260 | void die(const char *progname, const char *message) 261 | { 262 | fprintf(stderr, "%s: " RED "Error: " RESET "%s\n", progname, message); 263 | exit(1); 264 | } 265 | 266 | int main(int argc, char* argv[]) { 267 | //kill if root is executing 268 | if (geteuid() == 0) 269 | die(argv[0], "Do not run as root!"); 270 | 271 | //require args 272 | if (argc < 2) { 273 | help(argv[0]); 274 | return 1; 275 | } 276 | 277 | 278 | /* Initialize flags */ 279 | int Sflag = 0, Rflag = 0, sflag = 0, cflag = 0, bflag = 0, ch; 280 | 281 | /* now check options */ 282 | while ((ch = getopt(argc, argv, "hSRbsc")) != -1) { 283 | switch (ch) { 284 | case 'h': 285 | help(argv[0]); 286 | return 0; 287 | case 'S': 288 | Sflag = 1; 289 | break; 290 | case 'R': 291 | Rflag = 1; 292 | break; 293 | case 's': 294 | sflag = 1; 295 | break; 296 | case 'c': 297 | cflag = 1; 298 | break; 299 | case 'b': 300 | bflag = 1; 301 | break; 302 | default: 303 | help(argv[0]); 304 | return 1; 305 | } 306 | } 307 | 308 | /* run program */ 309 | 310 | if (Sflag && cflag) { 311 | char path[PATH_MAX]; 312 | snprintf(path, PATH_MAX, "%s/aureate", cache_path("XDG_CACHE_HOME")); 313 | 314 | printf("Are you sure you want to clean cache\nIn '%s' [y/n] ", path); 315 | 316 | char user_input = getchar(); 317 | if ( user_input == 'y' || user_input == 'Y') 318 | execlp("rm", "rm", "-rf", path, NULL); 319 | 320 | return 1; 321 | } 322 | 323 | if (Rflag && !Sflag) 324 | return execlp(SUDO, SUDO, "pacman", "-R", argv[2], NULL); 325 | else if (Sflag && sflag && !Rflag) 326 | search(argv[2]); 327 | else if (Sflag && !bflag && !Rflag) 328 | return download(argc, argv, 1); 329 | else if (Sflag && bflag && !Rflag) 330 | return download(argc, argv, 0); 331 | else 332 | die(argv[0], "Unknown option!"); 333 | 334 | 335 | 336 | return 0; 337 | } 338 | --------------------------------------------------------------------------------