這本書你已經(jīng)完成一半了,所以你需要做一個期中檢測。期中檢測中你需要重新構(gòu)建一個我特地為本書編寫的軟件,叫做devpkg。隨后你需要以一些方式擴展它,并且通過編寫一些單元測試來改進代碼。
注
我在一些你需要完成的練習(xí)之前編寫了這個練習(xí)。如果你現(xiàn)在嘗試這個練習(xí),記住軟件可能會含有一些bug,你可能由于我的錯誤會產(chǎn)生一些問題,也可能不知道需要什么來完成它。如果這樣的話,通過help@learncodethehardway.org來告訴我,之后等待我寫完其它練習(xí)。
devpkg?devpkg是一個簡單的C程序,可以用于安裝其它軟件。我特地為本書編寫了它,作為一種方式來教你真正的軟件是如何構(gòu)建的,以及如何復(fù)用他人的庫。它使用了一個叫做Apache可移植運行時(APR)的庫,其中含有許多工作跨平臺的便利的C函數(shù),包括Windows。此外,它只是從互聯(lián)網(wǎng)(或本地文件)抓取代碼,并且執(zhí)行通常的./configure ; make ; make install命令,每個程序員都用到過。
這個練習(xí)中,你的目標是從源碼構(gòu)建devpkg,完成我提供的每個挑戰(zhàn),并且使用源碼來理解devpkg做了什么和為什么這樣做。
我們打算創(chuàng)建一個具有三個命令的工具:
devpkg -S
在電腦上安裝新的軟件。
devpkg -I
從URL安裝軟件。
devpkg -L
列出安裝的所有軟件。
devpkg -F
為手動構(gòu)建抓取源代碼。
devpkg -B
構(gòu)建所抓取的源碼代碼并且安裝它,即使它已經(jīng)安裝了。
我們想讓devpkg能夠接受幾乎任何URL,判斷項目的類型,下載,安裝,以及注冊已經(jīng)安裝的軟件。我們也希望它能夠處理一個簡單的依賴列表,以便它能夠安裝項目所需的所有軟件。
為了完成這一目標,devpkg具有非常簡單的設(shè)計:
使用外部命令
大多數(shù)工作都是通過類似于curl、git和tar的外部命令完成的。這樣減少了devpkg所需的代碼量。
簡單的文件數(shù)據(jù)庫
你可以輕易使它變得很復(fù)雜,但是一開始你需要完成一個簡單的文件數(shù)據(jù)庫,位于/usr/local/.devpkg/db,來跟蹤已安裝的軟件。
/usr/local
同樣你可以使它更高級,但是對于初學(xué)者來說,假設(shè)項目始終位于/usr/local中,它是愛多數(shù)Unix軟件的標準安裝目錄。
configure; make; make install
假設(shè)大多數(shù)軟件可以通過configure; make; make install來安裝,也許configure是可選的。如果軟件不能通過這種方式安裝,要么提供某種方式來修改命令,要么devpkg就可以無視它。
用戶可以root
我們假設(shè)用于可以使用sudo來提升至root權(quán)限,除非他們直到最后才想root。
這會使我們的程序像當(dāng)初設(shè)想的一樣簡單,并且對于它的功能來說已經(jīng)足夠了。之后你可以進一步修改它。
你需要做的另外一件事情就是使用Apache可移植運行時(APR)來未完成這個練習(xí)獲得一個可移植的工具集。APR并不是必要的,你也可以不用它,但是你需要寫的代碼就會非常多。我現(xiàn)在強制你使用APR,使你能夠熟悉鏈接和使用其他的庫。最后,APR也能在Windows上工作,所以你可以把它遷移到許多其它平臺上。
你應(yīng)該獲取apr-1.4.5和apr-util-1.3的庫,以及瀏覽在apr.apache.org主站上的文檔。
下面是一個ShellScript,用于安裝所需的所有庫。你應(yīng)該手動將它寫到一個文件中,之后運行它直到APR安裝好并且沒有任何錯誤。
set -e
# go somewhere safe
cd /tmp
# get the source to base APR 1.4.6
curl -L -O http://archive.apache.org/dist/apr/apr-1.4.6.tar.gz
# extract it and go into the source
tar -xzvf apr-1.4.6.tar.gz
cd apr-1.4.6
# configure, make, make install
./configure
make
sudo make install
# reset and cleanup
cd /tmp
rm -rf apr-1.4.6 apr-1.4.6.tar.gz
# do the same with apr-util
curl -L -O http://archive.apache.org/dist/apr/apr-util-1.4.1.tar.gz
# extract
tar -xzvf apr-util-1.4.1.tar.gz
cd apr-util-1.4.1
# configure, make, make install
./configure --with-apr=/usr/local/apr
# you need that extra parameter to configure because
# apr-util can't really find it because...who knows.
make
sudo make install
#cleanup
cd /tmp
rm -rf apr-util-1.4.1* apr-1.4.6*
我希望你輸入這個腳本,因為這就是devpkg基本上所做的事情,只是帶有了一些選項和檢查項。實際上,你可以使用Shell以更少的代碼來完成它,但是這對于一本C語言的書不是一個很好的程序。
簡單運行這個腳本,修復(fù)它直到正常工作,就完成的所有庫的安裝,之后你需要完成項目的剩下部分。
你需要創(chuàng)建一些簡單的項目文件來起步。下面是我通常創(chuàng)建一個新項目的方法:
mkdir devpkg
cd devpkg
touch README Makefile
你應(yīng)該已經(jīng)安裝了APR和APR-util,所以你需要一些更多的文件作為基本的依賴:
dbg.h。bstrlib.h和bstrlib.c。下載.zip文件,解壓并且將這個兩個文件拷貝到項目中。make bstrlib.o,如果這不能正常工作,閱讀下面的“修復(fù)bstring”指南。注
在一些平臺上
bstring.c文件會出現(xiàn)下列錯誤:bstrlib.c:2762: error: expected declaration specifiers or '...' before numeric constant這是由于作者使用了一個不好的定義,它在一些平臺上不能工作。你需要修改第2759行的
#ifdef __GNUC__,并把它改成:#if defined(__GNUC__) && !defined(__APPLE__)
之后在Mac OSX平臺上就應(yīng)該能夠正常工作了。
做完上面這些后,你應(yīng)該有了Makefile,README,dbg.h,bstrlib.h和bstrlib.c,并做好了準備。
我們最好從Makefile開始,因為它列出了項目如何構(gòu)建,以及你會創(chuàng)建哪些源文件。
PREFIX?=/usr/local
CFLAGS=-g -Wall -I${PREFIX}/apr/include/apr-1 -I${PREFIX}/apr/include/apr-util-1
LDFLAGS=-L${PREFIX}/apr/lib -lapr-1 -pthread -laprutil-1
all: devpkg
devpkg: bstrlib.o db.o shell.o commands.o
install: all
install -d $(DESTDIR)/$(PREFIX)/bin/
install devpkg $(DESTDIR)/$(PREFIX)/bin/
clean:
rm -f *.o
rm -f devpkg
rm -rf *.dSYM
比起之前看到過的,這并沒有什么新東西,除了可能有些奇怪的?=語法,它表示“如果之前沒有定義,就將PREFIX設(shè)置為該值”。
注
如果你使用了最近版本的Ubuntu,你會得到
apr_off_t或off64_t的錯誤,之后需要向CFLAGS添加-D_LARGEFILE64_SOURCE=1。所需的另一件事是,你需要向
/etc/ld.conf.so.d/添加/usr/local/apr/lib,之后運行ldconfig使它能夠選擇正常的庫。
我們可以從makefile中看到,devpkg有四個依賴項,它們是:
bstrlib.o
由bstrlib.c和bstrlib.o產(chǎn)生,你已經(jīng)將它們引入了。
db.o
由db.c和db.h產(chǎn)生,它包含了一個小型“數(shù)據(jù)庫”程序集的代碼。
shell.o
由shell.c和shell.h產(chǎn)生,包含一些函數(shù),是類似curl的一些命令運行起來更容易。
commands.o
由commands.c和commands.h產(chǎn)生,包含了devpkg所需的所有命令并使它更易用。
devpkg
它不會顯式提到,但是它是Makefile在這一部分的目標。它由devpkg.c產(chǎn)生,包含用于整個程序的main函數(shù)。
你的任務(wù)就是創(chuàng)建這些文件,并且輸入代碼并保證正確。
注
你讀完這個描述可能會想,“Zed為什么那么聰明,坐著就能設(shè)計出來這些文件?!”我并不是用我強大的代碼功力魔術(shù)般地把
devpkg設(shè)計成這樣。而是我做了這些:
- 我編寫了簡單的
README來獲得如何構(gòu)建項目的靈感。- 我創(chuàng)建了一個簡單的bash腳本(就像你編寫的那樣)來理清所需的所有組件。
- 我創(chuàng)建了一個
.c文件,并且在它上面花了幾天,醞釀并想出點子。- 接著我編寫并調(diào)試程序,之后我將這一個大文件分成四個文件。
- 做完這些之后,我重命名和優(yōu)化了函數(shù)和數(shù)據(jù)結(jié)構(gòu),使它們在邏輯上更“美觀”。
- 最后,使新程序成功并以相同方式工作之后,我添加了一些新的特性,比如
-F和-B選項。你讀到的這份列表是我打算教給你的,但不要認為這是我構(gòu)建軟件的通用方法。有時候我會事先知道主題,并且會做更多的規(guī)劃。也有時我會編寫一份規(guī)劃并將它扔掉,之后再規(guī)劃更好的版本。它完全取決于我的經(jīng)驗告訴我哪個比較好,或者我的靈感將我?guī)У胶翁帯?/p>
如果你碰到一個“專家”,它告訴你只有一個方法可以解決編程問題,那么它在騙你。要么它們實際使用了很多策略,要么他們并不足夠好。
程序中必須有個方法來記錄已經(jīng)安裝的URL,列出這些URL,并且檢查一些程序是否已安裝以便跳過。我會使用一個簡單、扁平化的文件數(shù)據(jù)庫,以及bstrlib.h。
首先,創(chuàng)建db.h頭文件,以便讓你知道需要實現(xiàn)什么。
#ifndef _db_h
#define _db_h
#define DB_FILE "/usr/local/.devpkg/db"
#define DB_DIR "/usr/local/.devpkg"
int DB_init();
int DB_list();
int DB_update(const char *url);
int DB_find(const char *url);
#endif
之后實現(xiàn)db.c中的這些函數(shù),在你編寫它的時候,像之前一樣使用make。
#include <unistd.h>
#include <apr_errno.h>
#include <apr_file_io.h>
#include "db.h"
#include "bstrlib.h"
#include "dbg.h"
static FILE *DB_open(const char *path, const char *mode)
{
return fopen(path, mode);
}
static void DB_close(FILE *db)
{
fclose(db);
}
static bstring DB_load()
{
FILE *db = NULL;
bstring data = NULL;
db = DB_open(DB_FILE, "r");
check(db, "Failed to open database: %s", DB_FILE);
data = bread((bNread)fread, db);
check(data, "Failed to read from db file: %s", DB_FILE);
DB_close(db);
return data;
error:
if(db) DB_close(db);
if(data) bdestroy(data);
return NULL;
}
int DB_update(const char *url)
{
if(DB_find(url)) {
log_info("Already recorded as installed: %s", url);
}
FILE *db = DB_open(DB_FILE, "a+");
check(db, "Failed to open DB file: %s", DB_FILE);
bstring line = bfromcstr(url);
bconchar(line, '\n');
int rc = fwrite(line->data, blength(line), 1, db);
check(rc == 1, "Failed to append to the db.");
return 0;
error:
if(db) DB_close(db);
return -1;
}
int DB_find(const char *url)
{
bstring data = NULL;
bstring line = bfromcstr(url);
int res = -1;
data = DB_load();
check(data, "Failed to load: %s", DB_FILE);
if(binstr(data, 0, line) == BSTR_ERR) {
res = 0;
} else {
res = 1;
}
error: // fallthrough
if(data) bdestroy(data);
if(line) bdestroy(line);
return res;
}
int DB_init()
{
apr_pool_t *p = NULL;
apr_pool_initialize();
apr_pool_create(&p, NULL);
if(access(DB_DIR, W_OK | X_OK) == -1) {
apr_status_t rc = apr_dir_make_recursive(DB_DIR,
APR_UREAD | APR_UWRITE | APR_UEXECUTE |
APR_GREAD | APR_GWRITE | APR_GEXECUTE, p);
check(rc == APR_SUCCESS, "Failed to make database dir: %s", DB_DIR);
}
if(access(DB_FILE, W_OK) == -1) {
FILE *db = DB_open(DB_FILE, "w");
check(db, "Cannot open database: %s", DB_FILE);
DB_close(db);
}
apr_pool_destroy(p);
return 0;
error:
apr_pool_destroy(p);
return -1;
}
int DB_list()
{
bstring data = DB_load();
check(data, "Failed to read load: %s", DB_FILE);
printf("%s", bdata(data));
bdestroy(data);
return 0;
error:
return -1;
}
在繼續(xù)之前,仔細閱讀這些文件的每一行,并且確保你以準確地輸入了它們。通過逐行閱讀代碼來實踐它。同時,跟蹤每個函數(shù)調(diào)用,并且確保你使用了check來校驗返回值。最后,在APR網(wǎng)站上的文檔,或者bstrlib.h 或 bstrlib.c的源碼中,查閱每個你不認識的函數(shù)。
devkpg的一個關(guān)鍵設(shè)計是,使用類似于curl、tar和git的外部工具來完成大部分的工作。我們可以找到在程序內(nèi)部完成這些工作的庫,但是如果我們只是需要這些程序的基本功能,這樣就毫無意義。在Unix運行其它命令并不丟人。
為了完成這些,我打算使用apr_thread_proc.h函數(shù)來運行程序,但是我也希望創(chuàng)建一個簡單的類“模板”系統(tǒng)。我會使用struct Shell,它持有所有運行程序所需的信息,但是在參數(shù)中有一些“空位”,我可以將它們替換成實際值。
觀察shell.h文件來了解我會用到的結(jié)構(gòu)和命令。你可以看到我使用extern來表明其他的.c文件也能訪問到shell.c中定義的變量。
#ifndef _shell_h
#define _shell_h
#define MAX_COMMAND_ARGS 100
#include <apr_thread_proc.h>
typedef struct Shell {
const char *dir;
const char *exe;
apr_procattr_t *attr;
apr_proc_t proc;
apr_exit_why_e exit_why;
int exit_code;
const char *args[MAX_COMMAND_ARGS];
} Shell;
int Shell_run(apr_pool_t *p, Shell *cmd);
int Shell_exec(Shell cmd, ...);
extern Shell CLEANUP_SH;
extern Shell GIT_SH;
extern Shell TAR_SH;
extern Shell CURL_SH;
extern Shell CONFIGURE_SH;
extern Shell MAKE_SH;
extern Shell INSTALL_SH;
#endif
確保你已經(jīng)創(chuàng)建了shell.h,并且extern Shell變量的名字和數(shù)量相同。它們被Shell_run和Shell_exec函數(shù)用于運行命令。我定義了這兩個函數(shù),并且在shell.c中創(chuàng)建實際變量。
#include "shell.h"
#include "dbg.h"
#include <stdarg.h>
int Shell_exec(Shell template, ...)
{
apr_pool_t *p = NULL;
int rc = -1;
apr_status_t rv = APR_SUCCESS;
va_list argp;
const char *key = NULL;
const char *arg = NULL;
int i = 0;
rv = apr_pool_create(&p, NULL);
check(rv == APR_SUCCESS, "Failed to create pool.");
va_start(argp, template);
for(key = va_arg(argp, const char *);
key != NULL;
key = va_arg(argp, const char *))
{
arg = va_arg(argp, const char *);
for(i = 0; template.args[i] != NULL; i++) {
if(strcmp(template.args[i], key) == 0) {
template.args[i] = arg;
break; // found it
}
}
}
rc = Shell_run(p, &template);
apr_pool_destroy(p);
va_end(argp);
return rc;
error:
if(p) {
apr_pool_destroy(p);
}
return rc;
}
int Shell_run(apr_pool_t *p, Shell *cmd)
{
apr_procattr_t *attr;
apr_status_t rv;
apr_proc_t newproc;
rv = apr_procattr_create(&attr, p);
check(rv == APR_SUCCESS, "Failed to create proc attr.");
rv = apr_procattr_io_set(attr, APR_NO_PIPE, APR_NO_PIPE,
APR_NO_PIPE);
check(rv == APR_SUCCESS, "Failed to set IO of command.");
rv = apr_procattr_dir_set(attr, cmd->dir);
check(rv == APR_SUCCESS, "Failed to set root to %s", cmd->dir);
rv = apr_procattr_cmdtype_set(attr, APR_PROGRAM_PATH);
check(rv == APR_SUCCESS, "Failed to set cmd type.");
rv = apr_proc_create(&newproc, cmd->exe, cmd->args, NULL, attr, p);
check(rv == APR_SUCCESS, "Failed to run command.");
rv = apr_proc_wait(&newproc, &cmd->exit_code, &cmd->exit_why, APR_WAIT);
check(rv == APR_CHILD_DONE, "Failed to wait.");
check(cmd->exit_code == 0, "%s exited badly.", cmd->exe);
check(cmd->exit_why == APR_PROC_EXIT, "%s was killed or crashed", cmd->exe);
return 0;
error:
return -1;
}
Shell CLEANUP_SH = {
.exe = "rm",
.dir = "/tmp",
.args = {"rm", "-rf", "/tmp/pkg-build", "/tmp/pkg-src.tar.gz",
"/tmp/pkg-src.tar.bz2", "/tmp/DEPENDS", NULL}
};
Shell GIT_SH = {
.dir = "/tmp",
.exe = "git",
.args = {"git", "clone", "URL", "pkg-build", NULL}
};
Shell TAR_SH = {
.dir = "/tmp/pkg-build",
.exe = "tar",
.args = {"tar", "-xzf", "FILE", "--strip-components", "1", NULL}
};
Shell CURL_SH = {
.dir = "/tmp",
.exe = "curl",
.args = {"curl", "-L", "-o", "TARGET", "URL", NULL}
};
Shell CONFIGURE_SH = {
.exe = "./configure",
.dir = "/tmp/pkg-build",
.args = {"configure", "OPTS", NULL},
};
Shell MAKE_SH = {
.exe = "make",
.dir = "/tmp/pkg-build",
.args = {"make", "OPTS", NULL}
};
Shell INSTALL_SH = {
.exe = "sudo",
.dir = "/tmp/pkg-build",
.args = {"sudo", "make", "TARGET", NULL}
};
自底向上閱讀shell.c的代碼(這也是常見的C源碼布局),你會看到我創(chuàng)建了實際的Shell變量,它在shell.h中以extern修飾。它們雖然在這里,但是也被程序的其它部分使用。這就是創(chuàng)建全局變量的方式,它們可以存在于一個.c文件中,但是可在任何地方使用。你不應(yīng)該創(chuàng)建很多這類變量,但是它們的確很方便。
繼續(xù)閱讀代碼,我們讀到了Shell_run,它是一個“基”函數(shù),只是基于Shell中的東西執(zhí)行命令。它使用了許多在apr_thread_proc.h中定義的函數(shù),你需要查閱它們的每一個來了解工作原理。這就像是一些使用system函數(shù)調(diào)用的代碼一樣,但是它可以讓你控制其他程序的執(zhí)行。例如,在我們的Shell結(jié)構(gòu)中,存在.dir屬性在運行之前強制程序必須在指定目錄中。
最后,我創(chuàng)建了Shell_exec函數(shù),它是個變參函數(shù)。你在之前已經(jīng)看到過了,但是確保你理解了stdarg.h函數(shù)以及如何編寫它們。在下個挑戰(zhàn)中你需要分析這一函數(shù)。
Shell_exec為這些文件(以及向挑戰(zhàn)1那樣的完整的代碼復(fù)查)設(shè)置的挑戰(zhàn)是完整分析Shell_exec,并且拆分代碼來了解工作原理。你應(yīng)該能夠理解每一行代碼,for循環(huán)如何工作,以及參數(shù)如何被替換。
一旦你分析完成,向struct Shell添加一個字段,提供需要替代的args變量的數(shù)量。更新所有命令來接受參數(shù)的正確數(shù)量,隨后增加一個錯誤檢查,來確認參數(shù)被正確替換,以及在錯誤時退出。
現(xiàn)在你需要構(gòu)造正確的命令來完成功能。這些命令會用到APR的函數(shù)、db.h和shell.h來執(zhí)行下載和構(gòu)建軟件的真正工作。這些文件最為復(fù)雜,所以要小心編寫它們。你需要首先編寫commands.h文件,接著在commands.c文件中實現(xiàn)它的函數(shù)。
#ifndef _commands_h
#define _commands_h
#include <apr_pools.h>
#define DEPENDS_PATH "/tmp/DEPENDS"
#define TAR_GZ_SRC "/tmp/pkg-src.tar.gz"
#define TAR_BZ2_SRC "/tmp/pkg-src.tar.bz2"
#define BUILD_DIR "/tmp/pkg-build"
#define GIT_PAT "*.git"
#define DEPEND_PAT "*DEPENDS"
#define TAR_GZ_PAT "*.tar.gz"
#define TAR_BZ2_PAT "*.tar.bz2"
#define CONFIG_SCRIPT "/tmp/pkg-build/configure"
enum CommandType {
COMMAND_NONE, COMMAND_INSTALL, COMMAND_LIST, COMMAND_FETCH,
COMMAND_INIT, COMMAND_BUILD
};
int Command_fetch(apr_pool_t *p, const char *url, int fetch_only);
int Command_install(apr_pool_t *p, const char *url, const char *configure_opts,
const char *make_opts, const char *install_opts);
int Command_depends(apr_pool_t *p, const char *path);
int Command_build(apr_pool_t *p, const char *url, const char *configure_opts,
const char *make_opts, const char *install_opts);
#endif
commands.h中并沒有很多之前沒見過的東西。你應(yīng)該看到了一些字符串的定義,它們在任何地方都會用到。真正的代碼在commands.c中。
#include <apr_uri.h>
#include <apr_fnmatch.h>
#include <unistd.h>
#include "commands.h"
#include "dbg.h"
#include "bstrlib.h"
#include "db.h"
#include "shell.h"
int Command_depends(apr_pool_t *p, const char *path)
{
FILE *in = NULL;
bstring line = NULL;
in = fopen(path, "r");
check(in != NULL, "Failed to open downloaded depends: %s", path);
for(line = bgets((bNgetc)fgetc, in, '\n'); line != NULL;
line = bgets((bNgetc)fgetc, in, '\n'))
{
btrimws(line);
log_info("Processing depends: %s", bdata(line));
int rc = Command_install(p, bdata(line), NULL, NULL, NULL);
check(rc == 0, "Failed to install: %s", bdata(line));
bdestroy(line);
}
fclose(in);
return 0;
error:
if(line) bdestroy(line);
if(in) fclose(in);
return -1;
}
int Command_fetch(apr_pool_t *p, const char *url, int fetch_only)
{
apr_uri_t info = {.port = 0};
int rc = 0;
const char *depends_file = NULL;
apr_status_t rv = apr_uri_parse(p, url, &info);
check(rv == APR_SUCCESS, "Failed to parse URL: %s", url);
if(apr_fnmatch(GIT_PAT, info.path, 0) == APR_SUCCESS) {
rc = Shell_exec(GIT_SH, "URL", url, NULL);
check(rc == 0, "git failed.");
} else if(apr_fnmatch(DEPEND_PAT, info.path, 0) == APR_SUCCESS) {
check(!fetch_only, "No point in fetching a DEPENDS file.");
if(info.scheme) {
depends_file = DEPENDS_PATH;
rc = Shell_exec(CURL_SH, "URL", url, "TARGET", depends_file, NULL);
check(rc == 0, "Curl failed.");
} else {
depends_file = info.path;
}
// recursively process the devpkg list
log_info("Building according to DEPENDS: %s", url);
rv = Command_depends(p, depends_file);
check(rv == 0, "Failed to process the DEPENDS: %s", url);
// this indicates that nothing needs to be done
return 0;
} else if(apr_fnmatch(TAR_GZ_PAT, info.path, 0) == APR_SUCCESS) {
if(info.scheme) {
rc = Shell_exec(CURL_SH,
"URL", url,
"TARGET", TAR_GZ_SRC, NULL);
check(rc == 0, "Failed to curl source: %s", url);
}
rv = apr_dir_make_recursive(BUILD_DIR,
APR_UREAD | APR_UWRITE | APR_UEXECUTE, p);
check(rv == APR_SUCCESS, "Failed to make directory %s", BUILD_DIR);
rc = Shell_exec(TAR_SH, "FILE", TAR_GZ_SRC, NULL);
check(rc == 0, "Failed to untar %s", TAR_GZ_SRC);
} else if(apr_fnmatch(TAR_BZ2_PAT, info.path, 0) == APR_SUCCESS) {
if(info.scheme) {
rc = Shell_exec(CURL_SH, "URL", url, "TARGET", TAR_BZ2_SRC, NULL);
check(rc == 0, "Curl failed.");
}
apr_status_t rc = apr_dir_make_recursive(BUILD_DIR,
APR_UREAD | APR_UWRITE | APR_UEXECUTE, p);
check(rc == 0, "Failed to make directory %s", BUILD_DIR);
rc = Shell_exec(TAR_SH, "FILE", TAR_BZ2_SRC, NULL);
check(rc == 0, "Failed to untar %s", TAR_BZ2_SRC);
} else {
sentinel("Don't now how to handle %s", url);
}
// indicates that an install needs to actually run
return 1;
error:
return -1;
}
int Command_build(apr_pool_t *p, const char *url, const char *configure_opts,
const char *make_opts, const char *install_opts)
{
int rc = 0;
check(access(BUILD_DIR, X_OK | R_OK | W_OK) == 0,
"Build directory doesn't exist: %s", BUILD_DIR);
// actually do an install
if(access(CONFIG_SCRIPT, X_OK) == 0) {
log_info("Has a configure script, running it.");
rc = Shell_exec(CONFIGURE_SH, "OPTS", configure_opts, NULL);
check(rc == 0, "Failed to configure.");
}
rc = Shell_exec(MAKE_SH, "OPTS", make_opts, NULL);
check(rc == 0, "Failed to build.");
rc = Shell_exec(INSTALL_SH,
"TARGET", install_opts ? install_opts : "install",
NULL);
check(rc == 0, "Failed to install.");
rc = Shell_exec(CLEANUP_SH, NULL);
check(rc == 0, "Failed to cleanup after build.");
rc = DB_update(url);
check(rc == 0, "Failed to add this package to the database.");
return 0;
error:
return -1;
}
int Command_install(apr_pool_t *p, const char *url, const char *configure_opts,
const char *make_opts, const char *install_opts)
{
int rc = 0;
check(Shell_exec(CLEANUP_SH, NULL) == 0, "Failed to cleanup before building.");
rc = DB_find(url);
check(rc != -1, "Error checking the install database.");
if(rc == 1) {
log_info("Package %s already installed.", url);
return 0;
}
rc = Command_fetch(p, url, 0);
if(rc == 1) {
rc = Command_build(p, url, configure_opts, make_opts, install_opts);
check(rc == 0, "Failed to build: %s", url);
} else if(rc == 0) {
// no install needed
log_info("Depends successfully installed: %s", url);
} else {
// had an error
sentinel("Install failed: %s", url);
}
Shell_exec(CLEANUP_SH, NULL);
return 0;
error:
Shell_exec(CLEANUP_SH, NULL);
return -1;
}
在你輸入并編譯它之后,就可以開始分析了。如果到目前為止你完成了前面的挑戰(zhàn),你會理解如何使用shell.c函數(shù)來運行shell命令,以及參數(shù)如何被替換。如果沒有則需要回退到前面的挑戰(zhàn),確保你真正理解了Shell_exec的工作原理。
像之前一樣,完整地復(fù)查一遍代碼來保證一模一樣。接著瀏覽每個函數(shù)并且確保你知道他如何工作。你也應(yīng)該跟蹤這個文件或其它文件中,每個函數(shù)對其它函數(shù)的調(diào)用。最后,確認你理解了這里的所有調(diào)用APR的函數(shù)。
一旦你正確編寫并分析了這個文件,把我當(dāng)成一個傻瓜一樣來評判我的設(shè)計,我需要看看你是否可以改進它。不要真正修改代碼,只是創(chuàng)建一個notes.txt并且寫下你的想法和你需要修改的地方。
devpkg的main函數(shù)devpkg.c是最后且最重要的,但是也可能是最簡單的文件,其中創(chuàng)建了main函數(shù)。沒有與之配套的.h文件,因為這個文件包含其他所有文件。這個文件用于創(chuàng)建devpkg可執(zhí)行程序,同時組裝了來自Makefile的其它.o文件。在文件中輸入代碼并保證正確。
#include <stdio.h>
#include <apr_general.h>
#include <apr_getopt.h>
#include <apr_strings.h>
#include <apr_lib.h>
#include "dbg.h"
#include "db.h"
#include "commands.h"
int main(int argc, const char const *argv[])
{
apr_pool_t *p = NULL;
apr_pool_initialize();
apr_pool_create(&p, NULL);
apr_getopt_t *opt;
apr_status_t rv;
char ch = '\0';
const char *optarg = NULL;
const char *config_opts = NULL;
const char *install_opts = NULL;
const char *make_opts = NULL;
const char *url = NULL;
enum CommandType request = COMMAND_NONE;
rv = apr_getopt_init(&opt, p, argc, argv);
while(apr_getopt(opt, "I:Lc:m:i:d:SF:B:", &ch, &optarg) == APR_SUCCESS) {
switch (ch) {
case 'I':
request = COMMAND_INSTALL;
url = optarg;
break;
case 'L':
request = COMMAND_LIST;
break;
case 'c':
config_opts = optarg;
break;
case 'm':
make_opts = optarg;
break;
case 'i':
install_opts = optarg;
break;
case 'S':
request = COMMAND_INIT;
break;
case 'F':
request = COMMAND_FETCH;
url = optarg;
break;
case 'B':
request = COMMAND_BUILD;
url = optarg;
break;
}
}
switch(request) {
case COMMAND_INSTALL:
check(url, "You must at least give a URL.");
Command_install(p, url, config_opts, make_opts, install_opts);
break;
case COMMAND_LIST:
DB_list();
break;
case COMMAND_FETCH:
check(url != NULL, "You must give a URL.");
Command_fetch(p, url, 1);
log_info("Downloaded to %s and in /tmp/", BUILD_DIR);
break;
case COMMAND_BUILD:
check(url, "You must at least give a URL.");
Command_build(p, url, config_opts, make_opts, install_opts);
break;
case COMMAND_INIT:
rv = DB_init();
check(rv == 0, "Failed to make the database.");
break;
default:
sentinel("Invalid command given.");
}
return 0;
error:
return 1;
}
為這個文件設(shè)置的挑戰(zhàn)是理解參數(shù)如何處理,以及參數(shù)是什么,之后創(chuàng)建含有使用指南的README文件。在編寫README的同時,也編寫一個簡單的simple.sh,它運行./devpkg來檢查每個命令都在實際環(huán)境下工作。在你的腳本頂端使用set -e`,使它跳過第一個錯誤。
最后,在Valgrind下運行程序,確保在進行下一步之前,所有東西都能正常運行。
最后的挑戰(zhàn)就是這個期中檢測,它包含三件事情:
notes.txt中記錄你是如何改進代碼和devpkg的功能,并且實現(xiàn)你的改進。devpkg的替代版本,使用其他你喜歡的語言,或者你覺得最適合編寫它的語言。對比二者,之后基于你的結(jié)果改進你的devpkg的C版本。你可以執(zhí)行下列命令來將你的代碼與我的對比:
cd .. # get one directory above your current one
git clone git://gitorious.org/devpkg/devpkg.git devpkgzed
diff -r devpkg devpkgzed
這將會克隆我的devpkg版本到devpkgzed目錄中。之后使用工具diff來對比你的和我的代碼。書中你所使用的這些文件直接來自于這個項目,所以如果出現(xiàn)了不同的行,肯定就有錯誤。
要記住這個練習(xí)沒有真正的及格或不及格,它只是一個方式來讓你挑戰(zhàn)自己,并盡可能變得精確和謹慎。