练习26:编写第一个真正的程序

原文:Exercise 26: Write A First Real Program

译者:飞龙

这本书你已经完成一半了,所以你需要做一个期中检测。期中检测中你需要重新构建一个我特地为本书编写的软件,叫做devpkg。随后你需要以一些方式扩展它,并且通过编写一些单元测试来改进代码。

我在一些你需要完成的练习之前编写了这个练习。如果你现在尝试这个练习,记住软件可能会含有一些bug,你可能由于我的错误会产生一些问题,也可能不知道需要什么来完成它。如果这样的话,通过help@learncodethehardway.org来告诉我,之后等待我写完其它练习。

什么是devpkg

devpkg是一个简单的C程序,可以用于安装其它软件。我特地为本书编写了它,作为一种方式来教你真正的软件是如何构建的,以及如何复用他人的库。它使用了一个叫做Apache可移植运行时(APR)的库,其中含有许多工作跨平台的便利的C函数,包括Windows。此外,它只是从互联网(或本地文件)抓取代码,并且执行通常的./configure ; make ; make install命令,每个程序员都用到过。

这个练习中,你的目标是从源码构建devpkg,完成我提供的每个挑战,并且使用源码来理解devpkg做了什么和为什么这样做。

我们打算创建什么

我们打算创建一个具有三个命令的工具:

devpkg -S

在电脑上安装新的软件。

devpkg -I

从URL安装软件。

devpkg -L

列出安装的所有软件。

devpkg -F

为手动构建抓取源代码。

devpkg -B

构建所抓取的源码代码并且安装它,即使它已经安装了。

我们想让devpkg能够接受几乎任何URL,判断项目的类型,下载,安装,以及注册已经安装的软件。我们也希望它能够处理一个简单的依赖列表,以便它能够安装项目所需的所有软件。

设计

为了完成这一目标,devpkg具有非常简单的设计:

使用外部命令

大多数工作都是通过类似于curlgittar的外部命令完成的。这样减少了devpkg所需的代码量。

简单的文件数据库

你可以轻易使它变得很复杂,但是一开始你需要完成一个简单的文件数据库,位于/usr/local/.devpkg/db,来跟踪已安装的软件。

/usr/local

同样你可以使它更高级,但是对于初学者来说,假设项目始终位于/usr/local中,它是大多数Unix软件的标准安装目录。

configure; make; make install

假设大多数软件可以通过configure; make; make install来安装,也许configure是可选的。如果软件不能通过这种方式安装,要么提供某种方式来修改命令,要么devpkg就可以无视它。

用户可以root

我们假设用于可以使用sudo来提升至root权限,除非他们直到最后才想root。

这会使我们的程序像当初设想的一样简单,并且对于它的功能来说已经足够了。之后你可以进一步修改它。

Apache 可移植运行时

你需要做的另外一件事情就是使用Apache可移植运行时(APR)来未完成这个练习获得一个可移植的工具集。APR并不是必要的,你也可以不用它,但是你需要写的代码就会非常多。我现在强制你使用APR,使你能够熟悉链接和使用其他的库。最后,APR也能在Windows上工作,所以你可以把它迁移到许多其它平台上。

你应该获取apr-1.4.5apr-util-1.3的库,以及浏览在apr.apache.org主站上的文档。

下面是一个ShellScript,用于安装所需的所有库。你应该手动将它写到一个文件中,之后运行它直到APR安装好并且没有任何错误。

  1. set -e
  2. # go somewhere safe
  3. cd /tmp
  4. # get the source to base APR 1.4.6
  5. curl -L -O http://archive.apache.org/dist/apr/apr-1.4.6.tar.gz
  6. # extract it and go into the source
  7. tar -xzvf apr-1.4.6.tar.gz
  8. cd apr-1.4.6
  9. # configure, make, make install
  10. ./configure
  11. make
  12. sudo make install
  13. # reset and cleanup
  14. cd /tmp
  15. rm -rf apr-1.4.6 apr-1.4.6.tar.gz
  16. # do the same with apr-util
  17. curl -L -O http://archive.apache.org/dist/apr/apr-util-1.4.1.tar.gz
  18. # extract
  19. tar -xzvf apr-util-1.4.1.tar.gz
  20. cd apr-util-1.4.1
  21. # configure, make, make install
  22. ./configure --with-apr=/usr/local/apr
  23. # you need that extra parameter to configure because
  24. # apr-util can't really find it because...who knows.
  25. make
  26. sudo make install
  27. #cleanup
  28. cd /tmp
  29. rm -rf apr-util-1.4.1* apr-1.4.6*

我希望你输入这个脚本,因为这就是devpkg基本上所做的事情,只是带有了一些选项和检查项。实际上,你可以使用Shell以更少的代码来完成它,但是这对于一本C语言的书不是一个很好的程序。

简单运行这个脚本,修复它直到正常工作,就完成的所有库的安装,之后你需要完成项目的剩下部分。

项目布局

你需要创建一些简单的项目文件来起步。下面是我通常创建一个新项目的方法:

  1. mkdir devpkg
  2. cd devpkg
  3. touch README Makefile

其它依赖

你应该已经安装了APR和APR-util,所以你需要一些更多的文件作为基本的依赖:

  • 练习20中的dbg.h
  • http://bstring.sourceforge.net/下载的bstrlib.hbstrlib.c。下载.zip文件,解压并且将这个两个文件拷贝到项目中。
  • 运行make bstrlib.o,如果这不能正常工作,阅读下面的“修复bstring”指南。

在一些平台上bstring.c文件会出现下列错误:

  1. bstrlib.c:2762: error: expected declaration specifiers or '...' before numeric constant

这是由于作者使用了一个不好的定义,它在一些平台上不能工作。你需要修改第2759行的#ifdef __GNUC__,并把它改成:

  1. #if defined(__GNUC__) && !defined(__APPLE__)

之后在Mac OSX平台上就应该能够正常工作了。

做完上面这些后,你应该有了MakefileREADMEdbg.hbstrlib.hbstrlib.c,并做好了准备。

Makefile

我们最好从Makefile开始,因为它列出了项目如何构建,以及你会创建哪些源文件。

  1. PREFIX?=/usr/local
  2. CFLAGS=-g -Wall -I${PREFIX}/apr/include/apr-1 -I${PREFIX}/apr/include/apr-util-1
  3. LDFLAGS=-L${PREFIX}/apr/lib -lapr-1 -pthread -laprutil-1
  4. all: devpkg
  5. devpkg: bstrlib.o db.o shell.o commands.o
  6. install: all
  7. install -d $(DESTDIR)/$(PREFIX)/bin/
  8. install devpkg $(DESTDIR)/$(PREFIX)/bin/
  9. clean:
  10. rm -f *.o
  11. rm -f devpkg
  12. rm -rf *.dSYM

比起之前看到过的,这并没有什么新东西,除了可能有些奇怪的?=语法,它表示“如果之前没有定义,就将PREFIX设置为该值”。

如果你使用了最近版本的Ubuntu,你会得到apr_off_toff64_t的错误,之后需要向CFLAGS添加-D_LARGEFILE64_SOURCE=1

所需的另一件事是,你需要向/etc/ld.conf.so.d/添加/usr/local/apr/lib,之后运行ldconfig使它能够选择正常的库。

源文件

我们可以从makefile中看到,devpkg有四个依赖项,它们是:

bstrlib.o

bstrlib.cbstrlib.o产生,你已经将它们引入了。

db.o

db.cdb.h产生,它包含了一个小型“数据库”程序集的代码。

shell.o

shell.cshell.h产生,包含一些函数,是类似curl的一些命令运行起来更容易。

commands.o

commands.ccommands.h产生,包含了devpkg所需的所有命令并使它更易用。

devpkg

它不会显式提到,但是它是Makefile在这一部分的目标。它由devpkg.c产生,包含用于整个程序的main函数。

你的任务就是创建这些文件,并且输入代码并保证正确。

你读完这个描述可能会想,“Zed为什么那么聪明,坐着就能设计出来这些文件?!”我并不是用我强大的代码功力魔术般地把devpkg设计成这样。而是我做了这些:

  • 我编写了简单的README来获得如何构建项目的灵感。
  • 我创建了一个简单的bash脚本(就像你编写的那样)来理清所需的所有组件。
  • 我创建了一个.c文件,并且在它上面花了几天,酝酿并想出点子。
  • 接着我编写并调试程序,之后我将这一个大文件分成四个文件。
  • 做完这些之后,我重命名和优化了函数和数据结构,使它们在逻辑上更“美观”。
  • 最后,使新程序成功并以相同方式工作之后,我添加了一些新的特性,比如-F-B选项。

你读到的这份列表是我打算教给你的,但不要认为这是我构建软件的通用方法。有时候我会事先知道主题,并且会做更多的规划。也有时我会编写一份规划并将它扔掉,之后再规划更好的版本。它完全取决于我的经验告诉我哪个比较好,或者我的灵感将我带到何处。

如果你碰到一个“专家”,它告诉你只有一个方法可以解决编程问题,那么它在骗你。要么它们实际使用了很多策略,要么他们并不足够好。

DB函数

程序中必须有个方法来记录已经安装的URL,列出这些URL,并且检查一些程序是否已安装以便跳过。我会使用一个简单、扁平化的文件数据库,以及bstrlib.h

首先,创建db.h头文件,以便让你知道需要实现什么。

  1. #ifndef _db_h
  2. #define _db_h
  3. #define DB_FILE "/usr/local/.devpkg/db"
  4. #define DB_DIR "/usr/local/.devpkg"
  5. int DB_init();
  6. int DB_list();
  7. int DB_update(const char *url);
  8. int DB_find(const char *url);
  9. #endif

之后实现db.c中的这些函数,在你编写它的时候,像之前一样使用make

  1. #include <unistd.h>
  2. #include <apr_errno.h>
  3. #include <apr_file_io.h>
  4. #include "db.h"
  5. #include "bstrlib.h"
  6. #include "dbg.h"
  7. static FILE *DB_open(const char *path, const char *mode)
  8. {
  9. return fopen(path, mode);
  10. }
  11. static void DB_close(FILE *db)
  12. {
  13. fclose(db);
  14. }
  15. static bstring DB_load()
  16. {
  17. FILE *db = NULL;
  18. bstring data = NULL;
  19. db = DB_open(DB_FILE, "r");
  20. check(db, "Failed to open database: %s", DB_FILE);
  21. data = bread((bNread)fread, db);
  22. check(data, "Failed to read from db file: %s", DB_FILE);
  23. DB_close(db);
  24. return data;
  25. error:
  26. if(db) DB_close(db);
  27. if(data) bdestroy(data);
  28. return NULL;
  29. }
  30. int DB_update(const char *url)
  31. {
  32. if(DB_find(url)) {
  33. log_info("Already recorded as installed: %s", url);
  34. }
  35. FILE *db = DB_open(DB_FILE, "a+");
  36. check(db, "Failed to open DB file: %s", DB_FILE);
  37. bstring line = bfromcstr(url);
  38. bconchar(line, '\n');
  39. int rc = fwrite(line->data, blength(line), 1, db);
  40. check(rc == 1, "Failed to append to the db.");
  41. return 0;
  42. error:
  43. if(db) DB_close(db);
  44. return -1;
  45. }
  46. int DB_find(const char *url)
  47. {
  48. bstring data = NULL;
  49. bstring line = bfromcstr(url);
  50. int res = -1;
  51. data = DB_load();
  52. check(data, "Failed to load: %s", DB_FILE);
  53. if(binstr(data, 0, line) == BSTR_ERR) {
  54. res = 0;
  55. } else {
  56. res = 1;
  57. }
  58. error: // fallthrough
  59. if(data) bdestroy(data);
  60. if(line) bdestroy(line);
  61. return res;
  62. }
  63. int DB_init()
  64. {
  65. apr_pool_t *p = NULL;
  66. apr_pool_initialize();
  67. apr_pool_create(&p, NULL);
  68. if(access(DB_DIR, W_OK | X_OK) == -1) {
  69. apr_status_t rc = apr_dir_make_recursive(DB_DIR,
  70. APR_UREAD | APR_UWRITE | APR_UEXECUTE |
  71. APR_GREAD | APR_GWRITE | APR_GEXECUTE, p);
  72. check(rc == APR_SUCCESS, "Failed to make database dir: %s", DB_DIR);
  73. }
  74. if(access(DB_FILE, W_OK) == -1) {
  75. FILE *db = DB_open(DB_FILE, "w");
  76. check(db, "Cannot open database: %s", DB_FILE);
  77. DB_close(db);
  78. }
  79. apr_pool_destroy(p);
  80. return 0;
  81. error:
  82. apr_pool_destroy(p);
  83. return -1;
  84. }
  85. int DB_list()
  86. {
  87. bstring data = DB_load();
  88. check(data, "Failed to read load: %s", DB_FILE);
  89. printf("%s", bdata(data));
  90. bdestroy(data);
  91. return 0;
  92. error:
  93. return -1;
  94. }

挑战1:代码复查

在继续之前,仔细阅读这些文件的每一行,并且确保你以准确地输入了它们。通过逐行阅读代码来实践它。同时,跟踪每个函数调用,并且确保你使用了check来校验返回值。最后,在APR网站上的文档,或者bstrlib.h 或 bstrlib.c的源码中,查阅每个你不认识的函数。

Shell 函数

devkpg的一个关键设计是,使用类似于curltargit的外部工具来完成大部分的工作。我们可以找到在程序内部完成这些工作的库,但是如果我们只是需要这些程序的基本功能,这样就毫无意义。在Unix运行其它命令并不丢人。

为了完成这些,我打算使用apr_thread_proc.h函数来运行程序,但是我也希望创建一个简单的类“模板”系统。我会使用struct Shell,它持有所有运行程序所需的信息,但是在参数中有一些“空位”,我可以将它们替换成实际值。

观察shell.h文件来了解我会用到的结构和命令。你可以看到我使用extern来表明其他的.c文件也能访问到shell.c中定义的变量。

  1. #ifndef _shell_h
  2. #define _shell_h
  3. #define MAX_COMMAND_ARGS 100
  4. #include <apr_thread_proc.h>
  5. typedef struct Shell {
  6. const char *dir;
  7. const char *exe;
  8. apr_procattr_t *attr;
  9. apr_proc_t proc;
  10. apr_exit_why_e exit_why;
  11. int exit_code;
  12. const char *args[MAX_COMMAND_ARGS];
  13. } Shell;
  14. int Shell_run(apr_pool_t *p, Shell *cmd);
  15. int Shell_exec(Shell cmd, ...);
  16. extern Shell CLEANUP_SH;
  17. extern Shell GIT_SH;
  18. extern Shell TAR_SH;
  19. extern Shell CURL_SH;
  20. extern Shell CONFIGURE_SH;
  21. extern Shell MAKE_SH;
  22. extern Shell INSTALL_SH;
  23. #endif

确保你已经创建了shell.h,并且extern Shell变量的名字和数量相同。它们被Shell_runShell_exec函数用于运行命令。我定义了这两个函数,并且在shell.c中创建实际变量。

  1. #include "shell.h"
  2. #include "dbg.h"
  3. #include <stdarg.h>
  4. int Shell_exec(Shell template, ...)
  5. {
  6. apr_pool_t *p = NULL;
  7. int rc = -1;
  8. apr_status_t rv = APR_SUCCESS;
  9. va_list argp;
  10. const char *key = NULL;
  11. const char *arg = NULL;
  12. int i = 0;
  13. rv = apr_pool_create(&p, NULL);
  14. check(rv == APR_SUCCESS, "Failed to create pool.");
  15. va_start(argp, template);
  16. for(key = va_arg(argp, const char *);
  17. key != NULL;
  18. key = va_arg(argp, const char *))
  19. {
  20. arg = va_arg(argp, const char *);
  21. for(i = 0; template.args[i] != NULL; i++) {
  22. if(strcmp(template.args[i], key) == 0) {
  23. template.args[i] = arg;
  24. break; // found it
  25. }
  26. }
  27. }
  28. rc = Shell_run(p, &template);
  29. apr_pool_destroy(p);
  30. va_end(argp);
  31. return rc;
  32. error:
  33. if(p) {
  34. apr_pool_destroy(p);
  35. }
  36. return rc;
  37. }
  38. int Shell_run(apr_pool_t *p, Shell *cmd)
  39. {
  40. apr_procattr_t *attr;
  41. apr_status_t rv;
  42. apr_proc_t newproc;
  43. rv = apr_procattr_create(&attr, p);
  44. check(rv == APR_SUCCESS, "Failed to create proc attr.");
  45. rv = apr_procattr_io_set(attr, APR_NO_PIPE, APR_NO_PIPE,
  46. APR_NO_PIPE);
  47. check(rv == APR_SUCCESS, "Failed to set IO of command.");
  48. rv = apr_procattr_dir_set(attr, cmd->dir);
  49. check(rv == APR_SUCCESS, "Failed to set root to %s", cmd->dir);
  50. rv = apr_procattr_cmdtype_set(attr, APR_PROGRAM_PATH);
  51. check(rv == APR_SUCCESS, "Failed to set cmd type.");
  52. rv = apr_proc_create(&newproc, cmd->exe, cmd->args, NULL, attr, p);
  53. check(rv == APR_SUCCESS, "Failed to run command.");
  54. rv = apr_proc_wait(&newproc, &cmd->exit_code, &cmd->exit_why, APR_WAIT);
  55. check(rv == APR_CHILD_DONE, "Failed to wait.");
  56. check(cmd->exit_code == 0, "%s exited badly.", cmd->exe);
  57. check(cmd->exit_why == APR_PROC_EXIT, "%s was killed or crashed", cmd->exe);
  58. return 0;
  59. error:
  60. return -1;
  61. }
  62. Shell CLEANUP_SH = {
  63. .exe = "rm",
  64. .dir = "/tmp",
  65. .args = {"rm", "-rf", "/tmp/pkg-build", "/tmp/pkg-src.tar.gz",
  66. "/tmp/pkg-src.tar.bz2", "/tmp/DEPENDS", NULL}
  67. };
  68. Shell GIT_SH = {
  69. .dir = "/tmp",
  70. .exe = "git",
  71. .args = {"git", "clone", "URL", "pkg-build", NULL}
  72. };
  73. Shell TAR_SH = {
  74. .dir = "/tmp/pkg-build",
  75. .exe = "tar",
  76. .args = {"tar", "-xzf", "FILE", "--strip-components", "1", NULL}
  77. };
  78. Shell CURL_SH = {
  79. .dir = "/tmp",
  80. .exe = "curl",
  81. .args = {"curl", "-L", "-o", "TARGET", "URL", NULL}
  82. };
  83. Shell CONFIGURE_SH = {
  84. .exe = "./configure",
  85. .dir = "/tmp/pkg-build",
  86. .args = {"configure", "OPTS", NULL},
  87. };
  88. Shell MAKE_SH = {
  89. .exe = "make",
  90. .dir = "/tmp/pkg-build",
  91. .args = {"make", "OPTS", NULL}
  92. };
  93. Shell INSTALL_SH = {
  94. .exe = "sudo",
  95. .dir = "/tmp/pkg-build",
  96. .args = {"sudo", "make", "TARGET", NULL}
  97. };

自底向上阅读shell.c的代码(这也是常见的C源码布局),你会看到我创建了实际的Shell变量,它在shell.h中以extern修饰。它们虽然在这里,但是也被程序的其它部分使用。这就是创建全局变量的方式,它们可以存在于一个.c文件中,但是可在任何地方使用。你不应该创建很多这类变量,但是它们的确很方便。

继续阅读代码,我们读到了Shell_run,它是一个“基”函数,只是基于Shell中的东西执行命令。它使用了许多在apr_thread_proc.h中定义的函数,你需要查阅它们的每一个来了解工作原理。这就像是一些使用system函数调用的代码一样,但是它可以让你控制其他程序的执行。例如,在我们的Shell结构中,存在.dir属性在运行之前强制程序必须在指定目录中。

最后,我创建了Shell_exec函数,它是个变参函数。你在之前已经看到过了,但是确保你理解了stdarg.h函数以及如何编写它们。在下个挑战中你需要分析这一函数。

挑战2:分析Shell_exec

为这些文件(以及向挑战1那样的完整的代码复查)设置的挑战是完整分析Shell_exec,并且拆分代码来了解工作原理。你应该能够理解每一行代码,for循环如何工作,以及参数如何被替换。

一旦你分析完成,向struct Shell添加一个字段,提供需要替代的args变量的数量。更新所有命令来接受参数的正确数量,随后增加一个错误检查,来确认参数被正确替换,以及在错误时退出。

命令行函数

现在你需要构造正确的命令来完成功能。这些命令会用到APR的函数、db.hshell.h来执行下载和构建软件的真正工作。这些文件最为复杂,所以要小心编写它们。你需要首先编写commands.h文件,接着在commands.c文件中实现它的函数。

  1. #ifndef _commands_h
  2. #define _commands_h
  3. #include <apr_pools.h>
  4. #define DEPENDS_PATH "/tmp/DEPENDS"
  5. #define TAR_GZ_SRC "/tmp/pkg-src.tar.gz"
  6. #define TAR_BZ2_SRC "/tmp/pkg-src.tar.bz2"
  7. #define BUILD_DIR "/tmp/pkg-build"
  8. #define GIT_PAT "*.git"
  9. #define DEPEND_PAT "*DEPENDS"
  10. #define TAR_GZ_PAT "*.tar.gz"
  11. #define TAR_BZ2_PAT "*.tar.bz2"
  12. #define CONFIG_SCRIPT "/tmp/pkg-build/configure"
  13. enum CommandType {
  14. COMMAND_NONE, COMMAND_INSTALL, COMMAND_LIST, COMMAND_FETCH,
  15. COMMAND_INIT, COMMAND_BUILD
  16. };
  17. int Command_fetch(apr_pool_t *p, const char *url, int fetch_only);
  18. int Command_install(apr_pool_t *p, const char *url, const char *configure_opts,
  19. const char *make_opts, const char *install_opts);
  20. int Command_depends(apr_pool_t *p, const char *path);
  21. int Command_build(apr_pool_t *p, const char *url, const char *configure_opts,
  22. const char *make_opts, const char *install_opts);
  23. #endif

commands.h中并没有很多之前没见过的东西。你应该看到了一些字符串的定义,它们在任何地方都会用到。真正的代码在commands.c中。

  1. #include <apr_uri.h>
  2. #include <apr_fnmatch.h>
  3. #include <unistd.h>
  4. #include "commands.h"
  5. #include "dbg.h"
  6. #include "bstrlib.h"
  7. #include "db.h"
  8. #include "shell.h"
  9. int Command_depends(apr_pool_t *p, const char *path)
  10. {
  11. FILE *in = NULL;
  12. bstring line = NULL;
  13. in = fopen(path, "r");
  14. check(in != NULL, "Failed to open downloaded depends: %s", path);
  15. for(line = bgets((bNgetc)fgetc, in, '\n'); line != NULL;
  16. line = bgets((bNgetc)fgetc, in, '\n'))
  17. {
  18. btrimws(line);
  19. log_info("Processing depends: %s", bdata(line));
  20. int rc = Command_install(p, bdata(line), NULL, NULL, NULL);
  21. check(rc == 0, "Failed to install: %s", bdata(line));
  22. bdestroy(line);
  23. }
  24. fclose(in);
  25. return 0;
  26. error:
  27. if(line) bdestroy(line);
  28. if(in) fclose(in);
  29. return -1;
  30. }
  31. int Command_fetch(apr_pool_t *p, const char *url, int fetch_only)
  32. {
  33. apr_uri_t info = {.port = 0};
  34. int rc = 0;
  35. const char *depends_file = NULL;
  36. apr_status_t rv = apr_uri_parse(p, url, &info);
  37. check(rv == APR_SUCCESS, "Failed to parse URL: %s", url);
  38. if(apr_fnmatch(GIT_PAT, info.path, 0) == APR_SUCCESS) {
  39. rc = Shell_exec(GIT_SH, "URL", url, NULL);
  40. check(rc == 0, "git failed.");
  41. } else if(apr_fnmatch(DEPEND_PAT, info.path, 0) == APR_SUCCESS) {
  42. check(!fetch_only, "No point in fetching a DEPENDS file.");
  43. if(info.scheme) {
  44. depends_file = DEPENDS_PATH;
  45. rc = Shell_exec(CURL_SH, "URL", url, "TARGET", depends_file, NULL);
  46. check(rc == 0, "Curl failed.");
  47. } else {
  48. depends_file = info.path;
  49. }
  50. // recursively process the devpkg list
  51. log_info("Building according to DEPENDS: %s", url);
  52. rv = Command_depends(p, depends_file);
  53. check(rv == 0, "Failed to process the DEPENDS: %s", url);
  54. // this indicates that nothing needs to be done
  55. return 0;
  56. } else if(apr_fnmatch(TAR_GZ_PAT, info.path, 0) == APR_SUCCESS) {
  57. if(info.scheme) {
  58. rc = Shell_exec(CURL_SH,
  59. "URL", url,
  60. "TARGET", TAR_GZ_SRC, NULL);
  61. check(rc == 0, "Failed to curl source: %s", url);
  62. }
  63. rv = apr_dir_make_recursive(BUILD_DIR,
  64. APR_UREAD | APR_UWRITE | APR_UEXECUTE, p);
  65. check(rv == APR_SUCCESS, "Failed to make directory %s", BUILD_DIR);
  66. rc = Shell_exec(TAR_SH, "FILE", TAR_GZ_SRC, NULL);
  67. check(rc == 0, "Failed to untar %s", TAR_GZ_SRC);
  68. } else if(apr_fnmatch(TAR_BZ2_PAT, info.path, 0) == APR_SUCCESS) {
  69. if(info.scheme) {
  70. rc = Shell_exec(CURL_SH, "URL", url, "TARGET", TAR_BZ2_SRC, NULL);
  71. check(rc == 0, "Curl failed.");
  72. }
  73. apr_status_t rc = apr_dir_make_recursive(BUILD_DIR,
  74. APR_UREAD | APR_UWRITE | APR_UEXECUTE, p);
  75. check(rc == 0, "Failed to make directory %s", BUILD_DIR);
  76. rc = Shell_exec(TAR_SH, "FILE", TAR_BZ2_SRC, NULL);
  77. check(rc == 0, "Failed to untar %s", TAR_BZ2_SRC);
  78. } else {
  79. sentinel("Don't now how to handle %s", url);
  80. }
  81. // indicates that an install needs to actually run
  82. return 1;
  83. error:
  84. return -1;
  85. }
  86. int Command_build(apr_pool_t *p, const char *url, const char *configure_opts,
  87. const char *make_opts, const char *install_opts)
  88. {
  89. int rc = 0;
  90. check(access(BUILD_DIR, X_OK | R_OK | W_OK) == 0,
  91. "Build directory doesn't exist: %s", BUILD_DIR);
  92. // actually do an install
  93. if(access(CONFIG_SCRIPT, X_OK) == 0) {
  94. log_info("Has a configure script, running it.");
  95. rc = Shell_exec(CONFIGURE_SH, "OPTS", configure_opts, NULL);
  96. check(rc == 0, "Failed to configure.");
  97. }
  98. rc = Shell_exec(MAKE_SH, "OPTS", make_opts, NULL);
  99. check(rc == 0, "Failed to build.");
  100. rc = Shell_exec(INSTALL_SH,
  101. "TARGET", install_opts ? install_opts : "install",
  102. NULL);
  103. check(rc == 0, "Failed to install.");
  104. rc = Shell_exec(CLEANUP_SH, NULL);
  105. check(rc == 0, "Failed to cleanup after build.");
  106. rc = DB_update(url);
  107. check(rc == 0, "Failed to add this package to the database.");
  108. return 0;
  109. error:
  110. return -1;
  111. }
  112. int Command_install(apr_pool_t *p, const char *url, const char *configure_opts,
  113. const char *make_opts, const char *install_opts)
  114. {
  115. int rc = 0;
  116. check(Shell_exec(CLEANUP_SH, NULL) == 0, "Failed to cleanup before building.");
  117. rc = DB_find(url);
  118. check(rc != -1, "Error checking the install database.");
  119. if(rc == 1) {
  120. log_info("Package %s already installed.", url);
  121. return 0;
  122. }
  123. rc = Command_fetch(p, url, 0);
  124. if(rc == 1) {
  125. rc = Command_build(p, url, configure_opts, make_opts, install_opts);
  126. check(rc == 0, "Failed to build: %s", url);
  127. } else if(rc == 0) {
  128. // no install needed
  129. log_info("Depends successfully installed: %s", url);
  130. } else {
  131. // had an error
  132. sentinel("Install failed: %s", url);
  133. }
  134. Shell_exec(CLEANUP_SH, NULL);
  135. return 0;
  136. error:
  137. Shell_exec(CLEANUP_SH, NULL);
  138. return -1;
  139. }

在你输入并编译它之后,就可以开始分析了。如果到目前为止你完成了前面的挑战,你会理解如何使用shell.c函数来运行shell命令,以及参数如何被替换。如果没有则需要回退到前面的挑战,确保你真正理解了Shell_exec的工作原理。

挑战3:评判我的设计

像之前一样,完整地复查一遍代码来保证一模一样。接着浏览每个函数并且确保你知道他如何工作。你也应该跟踪这个文件或其它文件中,每个函数对其它函数的调用。最后,确认你理解了这里的所有调用APR的函数。

一旦你正确编写并分析了这个文件,把我当成一个傻瓜一样来评判我的设计,我需要看看你是否可以改进它。不要真正修改代码,只是创建一个notes.txt并且写下你的想法和你需要修改的地方。

devpkgmain函数

devpkg.c是最后且最重要的,但是也可能是最简单的文件,其中创建了main函数。没有与之配套的.h文件,因为这个文件包含其他所有文件。这个文件用于创建devpkg可执行程序,同时组装了来自Makefile的其它.o文件。在文件中输入代码并保证正确。

  1. #include <stdio.h>
  2. #include <apr_general.h>
  3. #include <apr_getopt.h>
  4. #include <apr_strings.h>
  5. #include <apr_lib.h>
  6. #include "dbg.h"
  7. #include "db.h"
  8. #include "commands.h"
  9. int main(int argc, const char const *argv[])
  10. {
  11. apr_pool_t *p = NULL;
  12. apr_pool_initialize();
  13. apr_pool_create(&p, NULL);
  14. apr_getopt_t *opt;
  15. apr_status_t rv;
  16. char ch = '\0';
  17. const char *optarg = NULL;
  18. const char *config_opts = NULL;
  19. const char *install_opts = NULL;
  20. const char *make_opts = NULL;
  21. const char *url = NULL;
  22. enum CommandType request = COMMAND_NONE;
  23. rv = apr_getopt_init(&opt, p, argc, argv);
  24. while(apr_getopt(opt, "I:Lc:m:i:d:SF:B:", &ch, &optarg) == APR_SUCCESS) {
  25. switch (ch) {
  26. case 'I':
  27. request = COMMAND_INSTALL;
  28. url = optarg;
  29. break;
  30. case 'L':
  31. request = COMMAND_LIST;
  32. break;
  33. case 'c':
  34. config_opts = optarg;
  35. break;
  36. case 'm':
  37. make_opts = optarg;
  38. break;
  39. case 'i':
  40. install_opts = optarg;
  41. break;
  42. case 'S':
  43. request = COMMAND_INIT;
  44. break;
  45. case 'F':
  46. request = COMMAND_FETCH;
  47. url = optarg;
  48. break;
  49. case 'B':
  50. request = COMMAND_BUILD;
  51. url = optarg;
  52. break;
  53. }
  54. }
  55. switch(request) {
  56. case COMMAND_INSTALL:
  57. check(url, "You must at least give a URL.");
  58. Command_install(p, url, config_opts, make_opts, install_opts);
  59. break;
  60. case COMMAND_LIST:
  61. DB_list();
  62. break;
  63. case COMMAND_FETCH:
  64. check(url != NULL, "You must give a URL.");
  65. Command_fetch(p, url, 1);
  66. log_info("Downloaded to %s and in /tmp/", BUILD_DIR);
  67. break;
  68. case COMMAND_BUILD:
  69. check(url, "You must at least give a URL.");
  70. Command_build(p, url, config_opts, make_opts, install_opts);
  71. break;
  72. case COMMAND_INIT:
  73. rv = DB_init();
  74. check(rv == 0, "Failed to make the database.");
  75. break;
  76. default:
  77. sentinel("Invalid command given.");
  78. }
  79. return 0;
  80. error:
  81. return 1;
  82. }

挑战4:README 和测试文件

为这个文件设置的挑战是理解参数如何处理,以及参数是什么,之后创建含有使用指南的README文件。在编写README的同时,也编写一个简单的simple.sh,它运行./devpkg来检查每个命令都在实际环境下工作。在你的脚本顶端使用set -e,使它跳过第一个错误。

最后,在Valgrind下运行程序,确保在进行下一步之前,所有东西都能正常运行。

期中检测

最后的挑战就是这个期中检测,它包含三件事情:

  • 将你的代码与我的在线代码对比,以100%的分数开始,每错一行减去1%。
  • 在你的notes.txt中记录你是如何改进代码和devpkg的功能,并且实现你的改进。
  • 编写一个devpkg的替代版本,使用其他你喜欢的语言,或者你觉得最适合编写它的语言。对比二者,之后基于你的结果改进你的devpkg的C版本。

你可以执行下列命令来将你的代码与我的对比:

  1. cd .. # get one directory above your current one
  2. git clone git://gitorious.org/devpkg/devpkg.git devpkgzed
  3. diff -r devpkg devpkgzed

这将会克隆我的devpkg版本到devpkgzed目录中。之后使用工具diff来对比你的和我的代码。书中你所使用的这些文件直接来自于这个项目,所以如果出现了不同的行,肯定就有错误。

要记住这个练习没有真正的及格或不及格,它只是一个方式来让你挑战自己,并尽可能变得精确和谨慎。