/* * CDDL HEADER START * * The contents of this file are subject to the terms of the * Common Development and Distribution License (the "License"). * You may not use this file except in compliance with the License. * * You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE * or http://www.opensolaris.org/os/licensing. * See the License for the specific language governing permissions * and limitations under the License. * * When distributing Covered Code, include this CDDL HEADER in each * file and include the License file at usr/src/OPENSOLARIS.LICENSE. * If applicable, add the following below this CDDL HEADER, with the * fields enclosed by brackets "[]" replaced with your own identifying * information: Portions Copyright [yyyy] [name of copyright owner] * * CDDL HEADER END * * Copyright 2009 Sun Microsystems, Inc. All rights reserved. * Use is subject to license terms. */ /* * Finds all unreferenced files in a source tree that do not match a list of * permitted pathnames. */ #include #include #include #include #include #include #include #include #include #include #include #include #include /* * Pathname set: a simple datatype for storing pathname pattern globs and * for checking whether a given pathname is matched by a pattern glob in * the set. */ typedef struct { char **paths; unsigned int npath; unsigned int maxpaths; } pnset_t; /* * Data associated with the current SCM manifest. */ typedef struct scmdata { pnset_t *manifest; char metapath[MAXPATHLEN]; char root[MAXPATHLEN]; unsigned int rootlen; boolean_t rootwarn; } scmdata_t; /* * Hooks used to check if a given unreferenced file is known to an SCM * (currently Git, Mercurial and TeamWare). */ typedef int checkscm_func_t(const char *, const struct FTW *); typedef void chdirscm_func_t(const char *); typedef struct { const char *name; checkscm_func_t *checkfunc; chdirscm_func_t *chdirfunc; } scm_t; static checkscm_func_t check_tw, check_scmdata; static chdirscm_func_t chdir_hg, chdir_git; static int pnset_add(pnset_t *, const char *); static int pnset_check(const pnset_t *, const char *); static void pnset_empty(pnset_t *); static void pnset_free(pnset_t *); static int checkpath(const char *, const struct stat *, int, struct FTW *); static pnset_t *make_exset(const char *); static void warn(const char *, ...); static void die(const char *, ...); static const scm_t scms[] = { { "tw", check_tw, NULL }, { "teamware", check_tw, NULL }, { "hg", check_scmdata, chdir_hg }, { "mercurial", check_scmdata, chdir_hg }, { "git", check_scmdata, chdir_git }, { NULL, NULL, NULL } }; static const scm_t *scm; static scmdata_t scmdata; static time_t tstamp; /* timestamp to compare files to */ static pnset_t *exsetp; /* pathname globs to ignore */ static const char *progname; int main(int argc, char *argv[]) { int c; char path[MAXPATHLEN]; char subtree[MAXPATHLEN] = "./"; char *tstampfile = ".build.tstamp"; struct stat tsstat; progname = strrchr(argv[0], '/'); if (progname == NULL) progname = argv[0]; else progname++; while ((c = getopt(argc, argv, "as:t:S:")) != EOF) { switch (c) { case 'a': /* for compatibility; now the default */ break; case 's': (void) strlcat(subtree, optarg, MAXPATHLEN); break; case 't': tstampfile = optarg; break; case 'S': for (scm = scms; scm->name != NULL; scm++) { if (strcmp(scm->name, optarg) == 0) break; } if (scm->name == NULL) die("unsupported SCM `%s'\n", optarg); break; default: case '?': goto usage; } } argc -= optind; argv += optind; if (argc != 2) { usage: (void) fprintf(stderr, "usage: %s [-s ] " "[-t ] [-S hg|tw|git] \n", progname); return (EXIT_FAILURE); } /* * Interpret a relative timestamp path as relative to srcroot. */ if (tstampfile[0] == '/') (void) strlcpy(path, tstampfile, MAXPATHLEN); else (void) snprintf(path, MAXPATHLEN, "%s/%s", argv[0], tstampfile); if (stat(path, &tsstat) == -1) die("cannot stat timestamp file \"%s\"", path); tstamp = tsstat.st_mtime; /* * Create the exception pathname set. */ exsetp = make_exset(argv[1]); if (exsetp == NULL) die("cannot make exception pathname set\n"); /* * Walk the specified subtree of the tree rooted at argv[0]. */ if (chdir(argv[0]) == -1) die("cannot change directory to \"%s\"", argv[0]); if (nftw(subtree, checkpath, 100, FTW_PHYS) != 0) die("cannot walk tree rooted at \"%s\"\n", argv[0]); pnset_empty(exsetp); return (EXIT_SUCCESS); } /* * Load and return a pnset for the manifest for the Mercurial repo at `hgroot'. */ static pnset_t * hg_manifest(const char *hgroot) { FILE *fp = NULL; char *hgcmd = NULL; char *newline; pnset_t *pnsetp; char path[MAXPATHLEN]; pnsetp = calloc(sizeof (pnset_t), 1); if (pnsetp == NULL || asprintf(&hgcmd, "hg manifest -R %s", hgroot) == -1) goto fail; fp = popen(hgcmd, "r"); if (fp == NULL) goto fail; while (fgets(path, sizeof (path), fp) != NULL) { newline = strrchr(path, '\n'); if (newline != NULL) *newline = '\0'; if (pnset_add(pnsetp, path) == 0) goto fail; } (void) pclose(fp); free(hgcmd); return (pnsetp); fail: warn("cannot load hg manifest at %s", hgroot); if (fp != NULL) (void) pclose(fp); free(hgcmd); pnset_free(pnsetp); return (NULL); } /* * Load and return a pnset for the manifest for the Git repo at `gitroot'. */ static pnset_t * git_manifest(const char *gitroot) { FILE *fp = NULL; char *gitcmd = NULL; char *newline; pnset_t *pnsetp; char path[MAXPATHLEN]; pnsetp = calloc(sizeof (pnset_t), 1); if (pnsetp == NULL || asprintf(&gitcmd, "git --git-dir=%s/.git ls-files", gitroot) == -1) goto fail; fp = popen(gitcmd, "r"); if (fp == NULL) goto fail; while (fgets(path, sizeof (path), fp) != NULL) { newline = strrchr(path, '\n'); if (newline != NULL) *newline = '\0'; if (pnset_add(pnsetp, path) == 0) goto fail; } (void) pclose(fp); free(gitcmd); return (pnsetp); fail: warn("cannot load git manifest at %s", gitroot); if (fp != NULL) (void) pclose(fp); free(gitcmd); pnset_free(pnsetp); return (NULL); } /* * If necessary, change our active manifest to be appropriate for `path'. */ static void chdir_scmdata(const char *path, const char *meta, pnset_t *(*manifest_func)(const char *path)) { char scmpath[MAXPATHLEN]; char basepath[MAXPATHLEN]; char *slash; (void) snprintf(scmpath, MAXPATHLEN, "%s/%s", path, meta); /* * Change our active manifest if any one of the following is true: * * 1. No manifest is loaded. Find the nearest SCM root to load from. * * 2. A manifest is loaded, but we've moved into a directory with * its own metadata directory (e.g., usr/closed). Load from its * root. * * 3. A manifest is loaded, but no longer applies (e.g., the manifest * under usr/closed is loaded, but we've moved to usr/src). */ if (scmdata.manifest == NULL || (strcmp(scmpath, scmdata.metapath) != 0 && access(scmpath, X_OK) == 0) || strncmp(path, scmdata.root, scmdata.rootlen - 1) != 0) { pnset_free(scmdata.manifest); scmdata.manifest = NULL; (void) strlcpy(basepath, path, MAXPATHLEN); /* * Walk up the directory tree looking for metadata * subdirectories. */ while (access(scmpath, X_OK) == -1) { slash = strrchr(basepath, '/'); if (slash == NULL) { if (!scmdata.rootwarn) { warn("no metadata directory " "for \"%s\"\n", path); scmdata.rootwarn = B_TRUE; } return; } *slash = '\0'; (void) snprintf(scmpath, MAXPATHLEN, "%s/%s", basepath, meta); } /* * We found a directory with an SCM metadata directory; record * it and load its manifest. */ (void) strlcpy(scmdata.metapath, scmpath, MAXPATHLEN); (void) strlcpy(scmdata.root, basepath, MAXPATHLEN); scmdata.manifest = manifest_func(scmdata.root); /* * The logic in check_scmdata() depends on scmdata.root having * a single trailing slash, so only add it if it's missing. */ if (scmdata.root[strlen(scmdata.root) - 1] != '/') (void) strlcat(scmdata.root, "/", MAXPATHLEN); scmdata.rootlen = strlen(scmdata.root); } } /* * If necessary, change our active manifest to be appropriate for `path'. */ static void chdir_git(const char *path) { chdir_scmdata(path, ".git", git_manifest); } static void chdir_hg(const char *path) { chdir_scmdata(path, ".hg", hg_manifest); } /* ARGSUSED */ static int check_scmdata(const char *path, const struct FTW *ftwp) { /* * The manifest paths are relative to the manifest root; skip past it. */ path += scmdata.rootlen; return (scmdata.manifest != NULL && pnset_check(scmdata.manifest, path)); } /* * Check if a file is under TeamWare control by checking for its corresponding * SCCS "s-dot" file. */ static int check_tw(const char *path, const struct FTW *ftwp) { char sccspath[MAXPATHLEN]; (void) snprintf(sccspath, MAXPATHLEN, "%.*s/SCCS/s.%s", ftwp->base, path, path + ftwp->base); return (access(sccspath, F_OK) == 0); } /* * Using `exceptfile' and a built-in list of exceptions, build and return a * pnset_t consisting of all of the pathnames globs which are allowed to be * unreferenced in the source tree. */ static pnset_t * make_exset(const char *exceptfile) { FILE *fp; char line[MAXPATHLEN]; char *newline; pnset_t *pnsetp; unsigned int i; pnsetp = calloc(sizeof (pnset_t), 1); if (pnsetp == NULL) return (NULL); /* * Add any exceptions from the file. */ fp = fopen(exceptfile, "r"); if (fp == NULL) { warn("cannot open exception file \"%s\"", exceptfile); goto fail; } while (fgets(line, sizeof (line), fp) != NULL) { newline = strrchr(line, '\n'); if (newline != NULL) *newline = '\0'; for (i = 0; isspace(line[i]); i++) ; if (line[i] == '#' || line[i] == '\0') continue; if (pnset_add(pnsetp, line) == 0) { (void) fclose(fp); goto fail; } } (void) fclose(fp); return (pnsetp); fail: pnset_free(pnsetp); return (NULL); } /* * FTW callback: print `path' if it's older than `tstamp' and not in `exsetp'. */ static int checkpath(const char *path, const struct stat *statp, int type, struct FTW *ftwp) { switch (type) { case FTW_F: /* * Skip if the file is referenced or in the exception list. */ if (statp->st_atime >= tstamp || pnset_check(exsetp, path)) return (0); /* * If requested, restrict ourselves to unreferenced files * under SCM control. */ if (scm == NULL || scm->checkfunc(path, ftwp)) (void) puts(path); return (0); case FTW_D: /* * Prune any directories in the exception list. */ if (pnset_check(exsetp, path)) { ftwp->quit = FTW_PRUNE; return (0); } /* * If necessary, advise the SCM logic of our new directory. */ if (scm != NULL && scm->chdirfunc != NULL) scm->chdirfunc(path); return (0); case FTW_DNR: warn("cannot read \"%s\"", path); return (0); case FTW_NS: warn("cannot stat \"%s\"", path); return (0); default: break; } return (0); } /* * Add `path' to the pnset_t pointed to by `pnsetp'. */ static int pnset_add(pnset_t *pnsetp, const char *path) { char **newpaths; unsigned int maxpaths; if (pnsetp->npath == pnsetp->maxpaths) { maxpaths = (pnsetp->maxpaths == 0) ? 512 : pnsetp->maxpaths * 2; newpaths = realloc(pnsetp->paths, sizeof (char *) * maxpaths); if (newpaths == NULL) return (0); pnsetp->paths = newpaths; pnsetp->maxpaths = maxpaths; } pnsetp->paths[pnsetp->npath] = strdup(path); if (pnsetp->paths[pnsetp->npath] == NULL) return (0); pnsetp->npath++; return (1); } /* * Check `path' against the pnset_t pointed to by `pnsetp'. */ static int pnset_check(const pnset_t *pnsetp, const char *path) { unsigned int i; for (i = 0; i < pnsetp->npath; i++) { if (fnmatch(pnsetp->paths[i], path, 0) == 0) return (1); } return (0); } /* * Empty the pnset_t pointed to by `pnsetp'. */ static void pnset_empty(pnset_t *pnsetp) { while (pnsetp->npath-- != 0) free(pnsetp->paths[pnsetp->npath]); free(pnsetp->paths); pnsetp->maxpaths = 0; } /* * Free the pnset_t pointed to by `pnsetp'. */ static void pnset_free(pnset_t *pnsetp) { if (pnsetp != NULL) { pnset_empty(pnsetp); free(pnsetp); } } /* PRINTFLIKE1 */ static void warn(const char *format, ...) { va_list alist; char *errstr = strerror(errno); if (errstr == NULL) errstr = ""; (void) fprintf(stderr, "%s: ", progname); va_start(alist, format); (void) vfprintf(stderr, format, alist); va_end(alist); if (strrchr(format, '\n') == NULL) (void) fprintf(stderr, ": %s\n", errstr); } /* PRINTFLIKE1 */ static void die(const char *format, ...) { va_list alist; char *errstr = strerror(errno); if (errstr == NULL) errstr = ""; (void) fprintf(stderr, "%s: fatal: ", progname); va_start(alist, format); (void) vfprintf(stderr, format, alist); va_end(alist); if (strrchr(format, '\n') == NULL) (void) fprintf(stderr, ": %s\n", errstr); exit(EXIT_FAILURE); }