/**************************************************************************** ** ** Copyright (C) 2014 Intel Corporation. ** Contact: http://www.qt-project.org/legal ** ** This file is part of the QtCore module of the Qt Toolkit. ** ** $QT_BEGIN_LICENSE:LGPL$ ** Commercial License Usage ** Licensees holding valid commercial Qt licenses may use this file in ** accordance with the commercial license agreement provided with the ** Software or, alternatively, in accordance with the terms contained in ** a written agreement between you and Digia. For licensing terms and ** conditions see http://qt.digia.com/licensing. For further information ** use the contact form at http://qt.digia.com/contact-us. ** ** GNU Lesser General Public License Usage ** Alternatively, this file may be used under the terms of the GNU Lesser ** General Public License version 2.1 as published by the Free Software ** Foundation and appearing in the file LICENSE.LGPL included in the ** packaging of this file. Please review the following information to ** ensure the GNU Lesser General Public License version 2.1 requirements ** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. ** ** In addition, as a special exception, Digia gives you certain additional ** rights. These rights are described in the Digia Qt LGPL Exception ** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. ** ** GNU General Public License Usage ** Alternatively, this file may be used under the terms of the GNU ** General Public License version 3.0 as published by the Free Software ** Foundation and appearing in the file LICENSE.GPL included in the ** packaging of this file. Please review the following information to ** ensure the GNU General Public License version 3.0 requirements will be ** met: http://www.gnu.org/copyleft/gpl.html. ** ** ** $QT_END_LICENSE$ ** ****************************************************************************/ /* * This tool is meant to wrap the other Qt tools by searching for different * instances of Qt on the system. It's mostly destined to be used on Unix * systems other than Mac OS X. For that reason, it uses POSIX APIs and does * not try to be compatible with the DOS or Win32 APIs. In particular, it uses * opendir(3) and readdir(3) as well as paths exclusively separated by slashes. */ #define _CRT_SECURE_NO_WARNINGS #define _POSIX_C_SOURCE 200809L #include #include #include #include #include #include #include #include #include #include #if defined(_WIN32) || defined(__WIN32__) # include # define execv _execv # define stat _stat # define PATH_SEP "\\" # define EXE_SUFFIX ".exe" #else # include # include # include # include # include # include # include # define PATH_SEP "/" # define EXE_SUFFIX "" #endif using namespace std; static const char myName[] = "qtchooser" EXE_SUFFIX; static const char confSuffix[] = ".conf"; static const char *argv0; enum Mode { Unknown, PrintHelp, RunTool, ListVersions, PrintEnvironment, Install }; enum InstallOptions { GlobalInstall = 0, LocalInstall = 1, NoOverwrite = 0, ForceOverwrite = 2 }; struct Sdk { string name; string configFile; string toolsPath; string librariesPath; bool isValid() const { return !toolsPath.empty(); } bool hasTool(const string &targetTool) const; }; bool Sdk::hasTool(const string &targetTool) const { struct stat st; if (toolsPath.empty()) return false; if (stat((toolsPath + PATH_SEP + targetTool).c_str(), &st)) return false; #ifdef S_IEXEC return (st.st_mode & S_IEXEC); #endif return true; } struct ToolWrapper { int printHelp(); int listVersions(); int printEnvironment(const string &targetSdk); int runTool(const string &targetSdk, const string &targetTool, char **argv); int install(const string &sdkName, const string &qmake, int installOptions); private: vector searchPaths() const; typedef bool (*VisitFunction)(const string &targetSdk, Sdk &item); typedef void (*FinishFunction)(const set &seenSdks); Sdk iterateSdks(const string &targetSdk, VisitFunction visit, FinishFunction finish = 0, const string &targetTool = ""); Sdk selectSdk(const string &targetSdk, const string &targetTool = ""); static void printSdks(const set &seenNames); static bool matchSdk(const string &targetSdk, Sdk &sdk); }; int ToolWrapper::printHelp() { puts("Usage:\n" " qtchooser { -l | -list-versions | -print-env }\n" " qtchooser -install [-f] [-local] \n" " qtchooser -run-tool= [-qt=] [program arguments]\n" " [-qt=] [program arguments]\n" "\n" "Environment variables accepted:\n" " QTCHOOSER_RUNTOOL name of the tool to be run (same as the -run-tool argument)\n" " QT_SELECT version of Qt to be run (same as the -qt argument)\n"); return 0; } int ToolWrapper::listVersions() { iterateSdks(string(), 0, &ToolWrapper::printSdks); return 0; } int ToolWrapper::printEnvironment(const string &targetSdk) { Sdk sdk = selectSdk(targetSdk); if (!sdk.isValid()) return 1; // ### The paths and the name are not escaped, so hopefully no one will be // installing Qt on paths containing backslashes or quotes on Unix. Paths // with spaces are taken into account by surrounding the arguments with // quotes. printf("QT_SELECT=\"%s\"\n", sdk.name.c_str()); printf("QTTOOLDIR=\"%s\"\n", sdk.toolsPath.c_str()); printf("QTLIBDIR=\"%s\"\n", sdk.librariesPath.c_str()); return 0; } static string userHome() { const char *value = getenv("HOME"); if (value) return value; #if defined(_WIN32) || defined(__WIN32__) // ### FIXME: some Windows-specific code to get the user's home directory // using GetUserProfileDirectory (userenv.h / dll) return "C:"; #else struct passwd *pwd = getpwuid(getuid()); if (pwd && pwd->pw_dir) return pwd->pw_dir; return string(); #endif } static inline bool beginsWith(const char *haystack, const char *needle) { return strncmp(haystack, needle, strlen(needle)) == 0; } static inline bool endsWith(const char *haystack, const char *needle) { size_t haystackLen = strlen(haystack); size_t needleLen = strlen(needle); if (needleLen > haystackLen) return false; return strcmp(haystack + haystackLen - needleLen, needle) == 0; } bool linksBackToSelf(const char *link, const char *target) { #if !defined(_WIN32) && !defined(__WIN32__) char buf[512]; int count = readlink(link, buf, sizeof(buf) - 1); if (count >= 0) { buf[count] = '\0'; if (endsWith(buf, target)) { fprintf(stderr, "%s: could not exec '%s' since it links to %s itself. Check your installation.\n", target, link, target); return true; } } #endif return false; } static bool readLine(FILE *f, string *result) { #if _POSIX_VERSION >= 200809L size_t len = 0; char *line = 0; ssize_t read = getline(&line, &len, f); if (read < 0) { free(line); return false; } line[strlen(line) - 1] = '\0'; *result = line; free(line); #elif defined(PATH_MAX) char buf[PATH_MAX]; if (!fgets(buf, PATH_MAX - 1, f)) return false; buf[PATH_MAX - 1] = '\0'; buf[strlen(buf) - 1] = '\0'; *result = buf; #else # error "POSIX < 2008 and no PATH_MAX, fix me" #endif return true; } static bool mkparentdir(string name) { // create the dir containing this dir size_t pos = name.rfind('/'); if (pos == string::npos) return false; name.erase(pos); if (mkdir(name.c_str(), 0777) == -1) { if (errno != ENOENT) return false; // try this dir's parent too if (!mkparentdir(name)) return false; } return true; } int ToolWrapper::runTool(const string &targetSdk, const string &targetTool, char **argv) { Sdk sdk = selectSdk(targetSdk, targetTool); if (!sdk.isValid()) return 1; string tool = sdk.toolsPath + PATH_SEP + targetTool; if (tool[0] == '~') tool = userHome() + tool.substr(1); // check if the tool is a symlink to ourselves if (linksBackToSelf(tool.c_str(), argv0)) return 1; argv[0] = &tool[0]; #ifdef QTCHOOSER_TEST_MODE while (*argv) printf("%s\n", *argv++); return 0; #else execv(argv[0], argv); #ifdef __APPLE__ // failed; see if we have a .app package by the same name { char appPath[PATH_MAX]; snprintf(appPath, PATH_MAX, "%s.app/Contents/MacOS/%s", argv[0], basename(argv[0])); execv(appPath, argv); } #endif fprintf(stderr, "%s: could not exec '%s': %s\n", argv0, argv[0], strerror(errno)); return 1; #endif } static const char *to_number(int number) { // obviously not thread-safe static char buffer[sizeof "2147483647"]; snprintf(buffer, sizeof buffer, "%d", number); return buffer; } int ToolWrapper::install(const string &sdkName, const string &qmake, int installOptions) { if (qmake.size() == 0) { fprintf(stderr, "%s: missing option: path to qmake\n", argv0); return 1; } if ((installOptions & ForceOverwrite) == 0) { Sdk matchedSdk = iterateSdks(sdkName, &ToolWrapper::matchSdk); if (matchedSdk.isValid()) { fprintf(stderr, "%s: SDK \"%s\" already exists\n", argv0, sdkName.c_str()); return 1; } } // first of all, get the bin and lib dirs from qmake string bindir, libdir; FILE *bin, *lib; bin = popen(("'" + qmake + "' -query QT_INSTALL_BINS").c_str(), "r"); lib = popen(("'" + qmake + "' -query QT_INSTALL_LIBS").c_str(), "r"); if (!readLine(bin, &bindir) || !readLine(lib, &libdir)\ || pclose(bin) == -1 || pclose(lib) == -1) { fprintf(stderr, "%s: error running %s: %s\n", argv0, qmake.c_str(), strerror(errno)); return 1; } const string sdkFileName = sdkName + confSuffix; const string fileContents = bindir + "\n" + libdir + "\n"; string sdkFullPath; // get the list of paths to try and install the SDK on the first we are able to; // since the list is sorted in search order, we need to try in the reverse order const vector paths = searchPaths(); vector::const_iterator it = paths.end(); vector::const_iterator prev = it - 1; for ( ; it != paths.begin(); it = prev--) { sdkFullPath = *prev + sdkFileName; // are we trying to install here? bool installHere = (installOptions & LocalInstall) == 0 || prev == paths.begin(); if (!installHere) continue; #ifdef QTCHOOSER_TEST_MODE puts(sdkFullPath.c_str()); puts(fileContents.c_str()); return 0; #else // we're good, create the SDK name string tempname = sdkFullPath + "." + to_number(rand()); int fd; // create a temporary file here while (true) { fd = ::open(tempname.c_str(), O_WRONLY | O_CREAT | O_EXCL, 0666); if (fd == -1 && errno == EEXIST) continue; if (fd == -1 && errno == ENOENT) { // could be because the dir itself doesn't exist if (mkparentdir(tempname)) continue; } break; } if (fd == -1) continue; size_t bytesWritten = 0; while (bytesWritten < fileContents.size()) { ssize_t written = ::write(fd, fileContents.data() + bytesWritten, fileContents.size() - bytesWritten); if (written == -1) { fprintf(stderr, "%s: error writing to \"%s\": %s\n", argv0, tempname.c_str(), strerror(errno)); ::close(fd); return 1; } bytesWritten += written; } // atomic rename ::close(fd); if (rename(tempname.c_str(), sdkFullPath.c_str()) == 0) return 0; // success #endif } // if we got here, we failed to create the file fprintf(stderr, "%s: could not create SDK: %s: %s\n", argv0, sdkFullPath.c_str(), strerror(errno)); return 1; } static vector stringSplit(const char *source) { #if defined(_WIN32) || defined(__WIN32__) char listSeparator = ';'; #else char listSeparator = ':'; #endif vector result; if (!*source) return result; while (true) { const char *p = strchr(source, listSeparator); if (!p) { result.push_back(source); return result; } result.push_back(string(source, p - source)); source = p + 1; } return result; } static string qgetenv(const char *env, const string &defaultValue = string()) { const char *value = getenv(env); return value ? string(value) : defaultValue; } vector ToolWrapper::searchPaths() const { vector paths; string localDir = qgetenv("XDG_CONFIG_HOME", userHome() + PATH_SEP ".config"); paths.push_back(localDir); // search the XDG config location directories vector xdgPaths = stringSplit(qgetenv("XDG_CONFIG_DIRS", "/etc/xdg").c_str()); paths.insert(paths.end(), xdgPaths.begin(), xdgPaths.end()); #if defined(QTCHOOSER_GLOBAL_DIR) if (qgetenv("QTCHOOSER_NO_GLOBAL_DIR").empty()) { vector globalPaths = stringSplit(QTCHOOSER_GLOBAL_DIR); paths.insert(paths.end(), globalPaths.begin(), globalPaths.end()); } #endif for (vector::iterator it = paths.begin(); it != paths.end(); ++it) *it += "/qtchooser/"; return paths; } Sdk ToolWrapper::iterateSdks(const string &targetSdk, VisitFunction visit, FinishFunction finish, const string &targetTool) { vector paths = searchPaths(); set seenNames; Sdk sdk; for (vector::iterator it = paths.begin(); it != paths.end(); ++it) { const string &path = *it; // no ISO C++ or ISO C API for listing directories, so use POSIX DIR *dir = opendir(path.c_str()); if (!dir) continue; // no such dir or not a dir, doesn't matter while (struct dirent *d = readdir(dir)) { #ifdef _DIRENT_HAVE_D_TYPE if (d->d_type == DT_DIR) continue; #endif size_t fnamelen = strlen(d->d_name); if (fnamelen < sizeof(confSuffix)) continue; if (memcmp(d->d_name + fnamelen + 1 - sizeof(confSuffix), confSuffix, sizeof confSuffix - 1) != 0) continue; if (seenNames.find(d->d_name) != seenNames.end()) continue; seenNames.insert(d->d_name); if (targetTool.empty()) { sdk.name = d->d_name; sdk.name.resize(fnamelen + 1 - sizeof confSuffix); } else { // To make the check in matchSdk() succeed sdk.name = "default"; } sdk.configFile = path + PATH_SEP + d->d_name; if (visit && visit(targetSdk, sdk)) { // If a tool was requested, but not found here, skip this sdk if (!targetTool.empty() && !sdk.hasTool(targetTool)) continue; return sdk; } } closedir(dir); } if (finish) finish(seenNames); return Sdk(); } // All tools that exist for only one Qt version should be // here. Other tools in this list are qdbus and qmlscene. bool fallbackAllowed(const string &tool) { return tool == "qdbus" || tool == "qml" || tool == "qmlimportscanner" || tool == "qmlscene" || tool == "qtdiag" || tool == "qtpaths" || tool == "qtplugininfo"; } Sdk ToolWrapper::selectSdk(const string &targetSdk, const string &targetTool) { // First, try the requested SDK Sdk matchedSdk = iterateSdks(targetSdk, &ToolWrapper::matchSdk); if (targetSdk.empty() && !matchedSdk.hasTool(targetTool) && fallbackAllowed(targetTool)) { // If a tool was requested, fall back to any SDK that has it matchedSdk = iterateSdks(string(), &ToolWrapper::matchSdk, 0, targetTool); } if (!matchedSdk.isValid()) { fprintf(stderr, "%s: could not find a Qt installation of '%s'\n", argv0, targetSdk.c_str()); } return matchedSdk; } void ToolWrapper::printSdks(const set &seenNames) { vector sorted; copy(seenNames.begin(), seenNames.end(), back_inserter(sorted)); sort(sorted.begin(), sorted.end()); vector::const_iterator it = sorted.begin(); for ( ; it != sorted.end(); ++it) { // strip the .conf suffix string s = *it; s.resize(s.size() - sizeof confSuffix + 1); printf("%s\n", s.c_str()); } } bool ToolWrapper::matchSdk(const string &targetSdk, Sdk &sdk) { if (targetSdk == sdk.name || (targetSdk.empty() && sdk.name == "default")) { FILE *f = fopen(sdk.configFile.c_str(), "r"); if (!f) { fprintf(stderr, "%s: could not open config file '%s': %s\n", argv0, sdk.configFile.c_str(), strerror(errno)); exit(1); } // read the first two lines. // 1) the first line contains the path to the Qt tools like qmake // 2) the second line contains the path to the Qt libraries // further lines are reserved for future enhancement if (!readLine(f, &sdk.toolsPath) || !readLine(f, &sdk.librariesPath)) { fclose(f); return false; } fclose(f); return true; } return false; } int main(int argc, char **argv) { // search the environment for defaults Mode operatingMode = Unknown; argv0 = basename(argv[0]); const char *targetSdk = getenv("QT_SELECT"); // the default tool is the one in argv[0] const char *targetTool = argv0; // if argv[0] points back to us, use the environment if (strcmp(targetTool, myName) == 0) targetTool = getenv("QTCHOOSER_RUNTOOL"); else operatingMode = RunTool; // check the arguments to see if there's an override int optind = 1; for ( ; optind < argc; ++optind) { char *arg = argv[optind]; if (*arg == '-') { ++arg; if (*arg == '-') ++arg; if (!*arg) { // -- argument, ends our processing // but skip this dash-dash argument ++optind; break; } else if (beginsWith(arg, "qt")) { // -qtX or -qt=X argument arg += 2; targetSdk = *arg == '=' ? arg + 1 : arg; } else if (!targetTool && beginsWith(arg, "run-tool=")) { // -run-tool= argument targetTool = arg + strlen("run-tool="); operatingMode = RunTool; } else { // not one of our arguments, must be for the target tool break; } } else { // not one of our arguments, must be for the target tool break; } } if (!targetSdk) targetSdk = ""; ToolWrapper wrapper; if (operatingMode == RunTool || targetTool) { if (!targetTool) { fprintf(stderr, "%s: no tool selected. Stop.\n", argv0); return 1; } return wrapper.runTool(targetSdk, targetTool, argv + optind - 1); } // running qtchooser itself // check for our arguments operatingMode = PrintHelp; int installOptions = 0; string sdkName; string qmakePath; for ( ; optind < argc; ++optind) { char *arg = argv[optind]; if (*arg == '-') { ++arg; if (*arg == '-') ++arg; // double-dash arguments are OK too if (*arg == '-') ++arg; if (strcmp(arg, "install") == 0) { operatingMode = Install; } else if (operatingMode == Install && (strcmp(arg, "force") == 0 || strcmp(arg, "f") == 0)) { installOptions |= ForceOverwrite; } else if (strcmp(arg, "list-versions") == 0 || strcmp(arg, "l") == 0) { operatingMode = ListVersions; } else if (operatingMode == Install && strcmp(arg, "local") == 0) { installOptions |= LocalInstall; } else if (beginsWith(arg, "print-env")) { operatingMode = PrintEnvironment; } else if (strcmp(arg, "help") != 0) { fprintf(stderr, "%s: unknown option: %s\n", argv0, arg - 1); return 1; } } else if (operatingMode == Install) { if (qmakePath.size()) { fprintf(stderr, "%s: install mode takes exactly two arguments; unknown option: %s\n", argv0, arg); return 1; } if (sdkName.size()) { qmakePath = arg; } else { sdkName = strlen(arg) ? arg : "default"; } } else { fprintf(stderr, "%s: unknown argument: %s\n", argv0, arg); return 1; } } // dispatch switch (operatingMode) { case RunTool: case Unknown: // can't happen! assert(false); return 127; case PrintHelp: return wrapper.printHelp(); case PrintEnvironment: return wrapper.printEnvironment(targetSdk); case ListVersions: return wrapper.listVersions(); case Install: return wrapper.install(sdkName, qmakePath, installOptions); } }