From c2d903f538792bdebeafc58f15cc7e30f23ea1ac Mon Sep 17 00:00:00 2001
From: Aymeric Wibo <obiwac@gmail.com>
Date: Sun, 24 Nov 2024 01:24:06 +0100
Subject: [PATCH 01/22] tests: Add `tests.sh` (simple testing framework)

---
 .gitignore       |  1 +
 tests.sh         | 32 ++++++++++++++++++++++++++++++++
 tests/bob.sh     |  7 +++++++
 tests/install.sh | 19 +++++++++++++++++++
 4 files changed, 59 insertions(+)
 create mode 100644 tests.sh
 create mode 100644 tests/bob.sh
 create mode 100644 tests/install.sh

diff --git a/.gitignore b/.gitignore
index ef9fb5e9..fe2e633b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -10,3 +10,4 @@ compile_commands.json
 .cache
 .bob
 .bootstrap
+.test-out
diff --git a/tests.sh b/tests.sh
new file mode 100644
index 00000000..f0fbec50
--- /dev/null
+++ b/tests.sh
@@ -0,0 +1,32 @@
+#!/bin/sh
+
+export ASAN_OPTIONS=detect_leaks=0 # XXX For now, let's not worry about leaks.
+export TEST_OUT=.test-out
+
+all_passed=1
+
+for test in $(ls -p tests | grep -v /); do
+	if [ -d $test ]; then
+		continue
+	fi
+
+	rm -rf .bob $TEST_OUT
+	mkdir -p $TEST_OUT
+
+	echo -n "Running test $test... "
+	sh tests/$test > /dev/null
+
+	if [ $? = 0 ]; then
+		echo "PASSED"
+	else
+		echo "FAILED"
+		all_passed=0
+	fi
+done
+
+if [ $all_passed = 0 ]; then
+	echo "TESTS FAILED!"
+	exit 1
+else
+	echo "ALL TESTS PASSED!"
+fi
diff --git a/tests/bob.sh b/tests/bob.sh
new file mode 100644
index 00000000..661d89be
--- /dev/null
+++ b/tests/bob.sh
@@ -0,0 +1,7 @@
+#!/bin/sh
+set -e
+
+# Test building Bob itself.
+
+.bootstrap/bob run -o $TEST_OUT/test-bob build
+.bootstrap/bob run -o $TEST_OUT/test-bob run -o $TEST_OUT/test-bob2 build
diff --git a/tests/install.sh b/tests/install.sh
new file mode 100644
index 00000000..b4f79422
--- /dev/null
+++ b/tests/install.sh
@@ -0,0 +1,19 @@
+#!/bin/sh
+set -e
+
+# Test installing programs.
+
+if which -s sudo; then
+	SUDO=sudo
+elif which -s doas; then
+	SUDO=doas
+else
+	echo "No sudo or doas found."
+	exit 1
+fi
+
+$SUDO .bootstrap/bob install
+bob build
+
+[ -d /usr/local/share/bob/skeletons ]
+[ -f /usr/local/bin/bob ]

From f55f11c76a2d223881969bb2b4521b8404cef683 Mon Sep 17 00:00:00 2001
From: Aymeric Wibo <obiwac@gmail.com>
Date: Sun, 24 Nov 2024 01:24:36 +0100
Subject: [PATCH 02/22] ci: Update to running `tests.sh` for the tests

---
 .cirrus.yml                 | 54 +++++++------------------------------
 .github/workflows/tests.yml | 32 +++-------------------
 2 files changed, 13 insertions(+), 73 deletions(-)

diff --git a/.cirrus.yml b/.cirrus.yml
index 93fa6f95..22561589 100644
--- a/.cirrus.yml
+++ b/.cirrus.yml
@@ -3,21 +3,10 @@ task:
   timeout_in: 5m
   container:
     image: gcc:latest
-  env:
-    ASAN_OPTIONS: detect_leaks=0
-  bootstrap_script:
+  bootstrap:
     - sh bootstrap.sh
-  test_build_bob_with_bob_script:
-    - $(realpath .bootstrap/bob) build
-  test_run_bob_with_bob_script:
-    - rm -rf .bob
-    - $(realpath .bootstrap/bob) run build
-  test_install_bob_script:
-    - rm -rf .bob
-    - $(realpath .bootstrap/bob) install
-  test_run_installed_bob_script:
-    - rm -rf .bob
-    - bob build
+  tests:
+    - sh tests.sh
   amd64_artifacts:
     path: ".bob/prefix/bin/bob"
 
@@ -26,21 +15,10 @@ task:
   timeout_in: 5m
   arm_container:
     image: gcc:latest
-  env:
-    ASAN_OPTIONS: detect_leaks=0
-  bootstrap_script:
+  bootstrap:
     - sh bootstrap.sh
-  test_build_bob_with_bob_script:
-    - $(realpath .bootstrap/bob) build
-  test_run_bob_with_bob_script:
-    - rm -rf .bob
-    - $(realpath .bootstrap/bob) run build
-  test_install_bob_script:
-    - rm -rf .bob
-    - $(realpath .bootstrap/bob) install
-  test_run_installed_bob_script:
-    - rm -rf .bob
-    - bob build
+  tests:
+    - sh tests.sh
   arm64_artifacts:
     path: ".bob/prefix/bin/bob"
 
@@ -54,23 +32,9 @@ compute_engine_instance:
 task:
   name: Test Bob with Clang on FreeBSD (x86_64)
   timeout_in: 30m
-  env:
-    ASAN_OPTIONS: detect_leaks=0
-  setup_script:
-    - uname -a
-    - df -h
-  bootstrap_script:
+  bootstrap:
     - sh bootstrap.sh
-  test_build_bob_with_bob_script:
-    - $(realpath .bootstrap/bob) build
-  test_run_bob_with_bob_script:
-    - rm -rf .bob
-    - $(realpath .bootstrap/bob) run build
-  test_install_bob_script:
-    - rm -rf .bob
-    - $(realpath .bootstrap/bob) install
-  test_run_installed_bob_script:
-    - rm -rf .bob
-    - bob build
+  tests:
+    - sh tests.sh
   amd64_artifacts:
     path: ".bob/prefix/bin/bob"
diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
index 2142b8ae..d5f947b6 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -16,20 +16,8 @@ jobs:
           platform: x64
       - name: Bootstrap
         run: sh bootstrap.sh
-      - name: Test building Bob using Bob
-        run: .bootstrap/bob build
-      - name: Test running the version of Bob built with Bob
-        run: |
-          rm -rf .bob
-          .bootstrap/bob run build
-      - name: Test installing Bob
-        run: |
-          rm -rf .bob
-          sudo -E .bootstrap/bob install
-      - name: Test running the installed version of Bob
-        run: |
-          rm -rf .bob
-          bob build
+      - name: Tests
+        run: sh tests.sh
       - name: Set up tmate session
         if: ${{ failure() }}
         uses: mxschmitt/action-tmate@v3
@@ -49,20 +37,8 @@ jobs:
       - uses: actions/checkout@v4
       - name: Bootstrap
         run: sh bootstrap.sh
-      - name: Test building Bob using Bob
-        run: .bootstrap/bob build
-      - name: Test running the version of Bob built with Bob
-        run: |
-          rm -rf .bob
-          .bootstrap/bob run build
-      - name: Test installing Bob
-        run: |
-          rm -rf .bob
-          sudo -E .bootstrap/bob install
-      - name: Test running the installed version of Bob
-        run: |
-          rm -rf .bob
-          bob build
+      - name: Tests
+        run: sh tests.sh
       - name: Set up tmate session
         if: ${{ failure() }}
         uses: mxschmitt/action-tmate@v3

From 8bf89a9d06d03a62b6de64f7c072e7fdd10f7133 Mon Sep 17 00:00:00 2001
From: Aymeric Wibo <obiwac@gmail.com>
Date: Sun, 24 Nov 2024 01:25:32 +0100
Subject: [PATCH 03/22] readme: Document testing

---
 README.md | 8 ++++++++
 1 file changed, 8 insertions(+)

diff --git a/README.md b/README.md
index cd95ec3e..990827df 100644
--- a/README.md
+++ b/README.md
@@ -51,3 +51,11 @@ To install a project with Bob, run:
 ```console
 bob install
 ```
+
+## Testing
+
+To run the Bob tests, simply run:
+
+```console
+sh tests.sh
+```

From 599b55e51ace082aaaad1a4db5c5848611a8aed7 Mon Sep 17 00:00:00 2001
From: Aymeric Wibo <obiwac@gmail.com>
Date: Sun, 24 Nov 2024 01:31:46 +0100
Subject: [PATCH 04/22] logging: Fix success and info logs erroneously
 outputting to `stderr`

---
 src/class/fs.c | 1 +
 src/logging.c  | 1 -
 src/logging.h  | 5 ++---
 3 files changed, 3 insertions(+), 4 deletions(-)

diff --git a/src/class/fs.c b/src/class/fs.c
index b04aa9e6..9ed04b17 100644
--- a/src/class/fs.c
+++ b/src/class/fs.c
@@ -10,6 +10,7 @@
 #include <errno.h>
 #include <fts.h>
 #include <stdio.h>
+#include <stdlib.h>
 #include <string.h>
 
 static flamingo_val_t* list_val = NULL;
diff --git a/src/logging.c b/src/logging.c
index fff45786..8274d8f6 100644
--- a/src/logging.c
+++ b/src/logging.c
@@ -10,7 +10,6 @@
 
 #include <assert.h>
 #include <err.h>
-#include <errno.h>
 #include <stdio.h>
 #include <stdlib.h>
 #include <string.h>
diff --git a/src/logging.h b/src/logging.h
index 1c7f8f09..ea895007 100644
--- a/src/logging.h
+++ b/src/logging.h
@@ -6,7 +6,6 @@
 #include <stdarg.h>
 #include <stdbool.h>
 #include <stdio.h>
-#include <stdlib.h>
 
 // external variables
 
@@ -56,5 +55,5 @@ void log_already_done(char const* cookie, char const* prefix, char const* past);
 #define LOG_FATAL(...) vlog(stderr, "💀 " BOLD PURPLE, __VA_ARGS__)
 #define LOG_ERROR(...) vlog(stderr, "🛑 " BOLD RED, __VA_ARGS__)
 #define LOG_WARN(...) vlog(stderr, "🚸 " REGULAR YELLOW, __VA_ARGS__)
-#define LOG_INFO(...) vlog(stderr, "👷 " REGULAR BLUE, __VA_ARGS__)
-#define LOG_SUCCESS(...) vlog(stderr, "✅ " REGULAR GREEN, __VA_ARGS__)
+#define LOG_INFO(...) vlog(stdout, "👷 " REGULAR BLUE, __VA_ARGS__)
+#define LOG_SUCCESS(...) vlog(stdout, "✅ " REGULAR GREEN, __VA_ARGS__)

From fac3379fa98cac4e44ae534ea1c011aa9cb40388 Mon Sep 17 00:00:00 2001
From: Aymeric Wibo <obiwac@gmail.com>
Date: Sun, 24 Nov 2024 15:36:36 +0100
Subject: [PATCH 05/22] tests: Move setting `$SUDO` to `tests.sh`

---
 tests.sh         | 9 +++++++++
 tests/install.sh | 9 ---------
 2 files changed, 9 insertions(+), 9 deletions(-)

diff --git a/tests.sh b/tests.sh
index f0fbec50..7af2fecb 100644
--- a/tests.sh
+++ b/tests.sh
@@ -3,6 +3,15 @@
 export ASAN_OPTIONS=detect_leaks=0 # XXX For now, let's not worry about leaks.
 export TEST_OUT=.test-out
 
+if which -s sudo; then
+	SUDO=sudo
+elif which -s doas; then
+	SUDO=doas
+else
+	echo "No sudo or doas found."
+	exit 1
+fi
+
 all_passed=1
 
 for test in $(ls -p tests | grep -v /); do
diff --git a/tests/install.sh b/tests/install.sh
index b4f79422..bea5f6be 100644
--- a/tests/install.sh
+++ b/tests/install.sh
@@ -3,15 +3,6 @@ set -e
 
 # Test installing programs.
 
-if which -s sudo; then
-	SUDO=sudo
-elif which -s doas; then
-	SUDO=doas
-else
-	echo "No sudo or doas found."
-	exit 1
-fi
-
 $SUDO .bootstrap/bob install
 bob build
 

From 59aedce92abe456662a54c9d1b06424fcb3b6581 Mon Sep 17 00:00:00 2001
From: Aymeric Wibo <obiwac@gmail.com>
Date: Sun, 24 Nov 2024 15:36:56 +0100
Subject: [PATCH 06/22] tests: Add chown regression test for #69

---
 tests/chown.sh       | 47 ++++++++++++++++++++++++++++++++++++++++++++
 tests/chown/build.fl |  8 ++++++++
 2 files changed, 55 insertions(+)
 create mode 100644 tests/chown.sh
 create mode 100644 tests/chown/build.fl

diff --git a/tests/chown.sh b/tests/chown.sh
new file mode 100644
index 00000000..267dff4c
--- /dev/null
+++ b/tests/chown.sh
@@ -0,0 +1,47 @@
+#!/bin/sh
+set -e
+
+# Regression test for the following PR:
+# https://github.com/inobulles/bob/pull/69
+# Basically, there's this feature which chown's generated files to the owner of the project directory when running Bob as root.
+# The issue though is that, before #69, even installed files would be chown'd to the owner of the project directory.
+# The result was that on FreeBSD, the ldconfig service would refuse to cache the libraries in /usr/local/lib because they weren't owned by root, which is obviously very bad as nothing that linked against those would work.
+
+# Make sure everything is clean.
+
+INSTALL_PATH=/usr/local/share/bob_chown_test.fl
+$SUDO rm -rf tests/chown/.bob $INSTALL_PATH
+
+# Create a user.
+
+USER=bobtestuser
+
+if [ $(uname) == "Linux" ] || [ $(uname) == "FreeBSD" ]; then
+	useradd -m $USER
+elif [ $(uname) == "Darwin" ]; then
+	$SUDO sysadminctl -addUser $USER
+else
+	echo "Unsupported OS (don't know how to create a user)."
+	exit 1
+fi
+
+# Chown the project directory to that user.
+
+$SUDO chown -R bobtestuser tests/chown
+$SUDO .bootstrap/bob -C tests/chown build
+
+if [ ! -z "$(find tests/chown ! -user bobtestuser)" ]; then
+	echo "Some files are not owned by bobtestuser in tests/chown."
+	exit 1
+fi
+
+$SUDO .bootstrap/bob -C tests/chown install
+
+# XXX Annoyingly, 'stat -f %Su' doesn't work on Linux (you have to use 'stat -c %U').
+
+owner=$(ls -ld /usr/local/share/bob_chown_test.fl | awk '{print $3}')
+
+if [ $owner != "root" ]; then
+	echo "Installed files are not owned by root."
+	exit 1
+fi
diff --git a/tests/chown/build.fl b/tests/chown/build.fl
new file mode 100644
index 00000000..01d5b80a
--- /dev/null
+++ b/tests/chown/build.fl
@@ -0,0 +1,8 @@
+# SPDX-License-Identifier: MIT
+# Copyright (c) 2024 Aymeric Wibo
+
+import bob
+
+install = {
+	"build.fl": "share/bob_chown_test.fl",
+}

From b7edd2a08c2035a1bac98ed8c8e3ae5323a24d87 Mon Sep 17 00:00:00 2001
From: Aymeric Wibo <obiwac@gmail.com>
Date: Sun, 24 Nov 2024 15:39:03 +0100
Subject: [PATCH 07/22] ci(cirrus): Fix bootstrap and test scripts

---
 .cirrus.yml | 12 ++++++------
 1 file changed, 6 insertions(+), 6 deletions(-)

diff --git a/.cirrus.yml b/.cirrus.yml
index 22561589..ed6f83d2 100644
--- a/.cirrus.yml
+++ b/.cirrus.yml
@@ -3,9 +3,9 @@ task:
   timeout_in: 5m
   container:
     image: gcc:latest
-  bootstrap:
+  bootstrap_script:
     - sh bootstrap.sh
-  tests:
+  tests_script:
     - sh tests.sh
   amd64_artifacts:
     path: ".bob/prefix/bin/bob"
@@ -15,9 +15,9 @@ task:
   timeout_in: 5m
   arm_container:
     image: gcc:latest
-  bootstrap:
+  bootstrap_script:
     - sh bootstrap.sh
-  tests:
+  tests_script:
     - sh tests.sh
   arm64_artifacts:
     path: ".bob/prefix/bin/bob"
@@ -32,9 +32,9 @@ compute_engine_instance:
 task:
   name: Test Bob with Clang on FreeBSD (x86_64)
   timeout_in: 30m
-  bootstrap:
+  bootstrap_script:
     - sh bootstrap.sh
-  tests:
+  tests_script:
     - sh tests.sh
   amd64_artifacts:
     path: ".bob/prefix/bin/bob"

From 320ff51d666ffed6a7005fa9efba1adafbcc5e4f Mon Sep 17 00:00:00 2001
From: Aymeric Wibo <obiwac@gmail.com>
Date: Sun, 24 Nov 2024 15:43:13 +0100
Subject: [PATCH 08/22] tests: Export `$SUDO`

---
 tests.sh | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/tests.sh b/tests.sh
index 7af2fecb..8e1dc2cf 100644
--- a/tests.sh
+++ b/tests.sh
@@ -4,9 +4,9 @@ export ASAN_OPTIONS=detect_leaks=0 # XXX For now, let's not worry about leaks.
 export TEST_OUT=.test-out
 
 if which -s sudo; then
-	SUDO=sudo
+	export SUDO=sudo
 elif which -s doas; then
-	SUDO=doas
+	export SUDO=doas
 else
 	echo "No sudo or doas found."
 	exit 1

From 6c450c3517d74215d38fc9f996d8ad051727e986 Mon Sep 17 00:00:00 2001
From: Aymeric Wibo <obiwac@gmail.com>
Date: Sun, 24 Nov 2024 15:44:42 +0100
Subject: [PATCH 09/22] tests: Fix for sudo on Linux

---
 tests.sh | 5 ++++-
 1 file changed, 4 insertions(+), 1 deletion(-)

diff --git a/tests.sh b/tests.sh
index 8e1dc2cf..a48c391f 100644
--- a/tests.sh
+++ b/tests.sh
@@ -3,10 +3,13 @@
 export ASAN_OPTIONS=detect_leaks=0 # XXX For now, let's not worry about leaks.
 export TEST_OUT=.test-out
 
-if which -s sudo; then
+if [ $(uname) = "Linux" ]; then
+	# XXX Linux is annoying, 'which -s' doesn't exist.
 	export SUDO=sudo
 elif which -s doas; then
 	export SUDO=doas
+elif which -s sudo; then
+	export SUDO=sudo
 else
 	echo "No sudo or doas found."
 	exit 1

From bc4b7e1f2e2f643f6ec67d103c50ebbebf21926d Mon Sep 17 00:00:00 2001
From: Aymeric Wibo <obiwac@gmail.com>
Date: Sun, 24 Nov 2024 15:47:12 +0100
Subject: [PATCH 10/22] tests: Install bob first

---
 tests.sh         | 8 ++++++++
 tests/bob.sh     | 4 ++--
 tests/chown.sh   | 4 ++--
 tests/install.sh | 2 +-
 4 files changed, 13 insertions(+), 5 deletions(-)

diff --git a/tests.sh b/tests.sh
index a48c391f..cbdd49f5 100644
--- a/tests.sh
+++ b/tests.sh
@@ -3,6 +3,8 @@
 export ASAN_OPTIONS=detect_leaks=0 # XXX For now, let's not worry about leaks.
 export TEST_OUT=.test-out
 
+# Find doas or sudo.
+
 if [ $(uname) = "Linux" ]; then
 	# XXX Linux is annoying, 'which -s' doesn't exist.
 	export SUDO=sudo
@@ -15,6 +17,12 @@ else
 	exit 1
 fi
 
+# Install Bob.
+
+$SUDO .bootstrap/bob install
+
+# Actually run the tests.
+
 all_passed=1
 
 for test in $(ls -p tests | grep -v /); do
diff --git a/tests/bob.sh b/tests/bob.sh
index 661d89be..725493b9 100644
--- a/tests/bob.sh
+++ b/tests/bob.sh
@@ -3,5 +3,5 @@ set -e
 
 # Test building Bob itself.
 
-.bootstrap/bob run -o $TEST_OUT/test-bob build
-.bootstrap/bob run -o $TEST_OUT/test-bob run -o $TEST_OUT/test-bob2 build
+bob run -o $TEST_OUT/test-bob build
+bob run -o $TEST_OUT/test-bob run -o $TEST_OUT/test-bob2 build
diff --git a/tests/chown.sh b/tests/chown.sh
index 267dff4c..070109d6 100644
--- a/tests/chown.sh
+++ b/tests/chown.sh
@@ -28,14 +28,14 @@ fi
 # Chown the project directory to that user.
 
 $SUDO chown -R bobtestuser tests/chown
-$SUDO .bootstrap/bob -C tests/chown build
+$SUDO bob -C tests/chown build
 
 if [ ! -z "$(find tests/chown ! -user bobtestuser)" ]; then
 	echo "Some files are not owned by bobtestuser in tests/chown."
 	exit 1
 fi
 
-$SUDO .bootstrap/bob -C tests/chown install
+$SUDO bob -C tests/chown install
 
 # XXX Annoyingly, 'stat -f %Su' doesn't work on Linux (you have to use 'stat -c %U').
 
diff --git a/tests/install.sh b/tests/install.sh
index bea5f6be..867a267d 100644
--- a/tests/install.sh
+++ b/tests/install.sh
@@ -3,7 +3,7 @@ set -e
 
 # Test installing programs.
 
-$SUDO .bootstrap/bob install
+$SUDO bob install
 bob build
 
 [ -d /usr/local/share/bob/skeletons ]

From eec37c55588c679c24cdc482c2c4f25d7f0331b9 Mon Sep 17 00:00:00 2001
From: Aymeric Wibo <obiwac@gmail.com>
Date: Sun, 24 Nov 2024 15:50:10 +0100
Subject: [PATCH 11/22] tests/chown: Fix operators

---
 tests.sh       | 3 ++-
 tests/chown.sh | 4 ++--
 2 files changed, 4 insertions(+), 3 deletions(-)

diff --git a/tests.sh b/tests.sh
index cbdd49f5..6a5541bd 100644
--- a/tests.sh
+++ b/tests.sh
@@ -19,7 +19,8 @@ fi
 
 # Install Bob.
 
-$SUDO .bootstrap/bob install
+sh bootstrap.sh
+$SUDO .bootstrap/bob install > /dev/null
 
 # Actually run the tests.
 
diff --git a/tests/chown.sh b/tests/chown.sh
index 070109d6..e28040c2 100644
--- a/tests/chown.sh
+++ b/tests/chown.sh
@@ -16,9 +16,9 @@ $SUDO rm -rf tests/chown/.bob $INSTALL_PATH
 
 USER=bobtestuser
 
-if [ $(uname) == "Linux" ] || [ $(uname) == "FreeBSD" ]; then
+if [ $(uname) = "Linux" ] || [ $(uname) = "FreeBSD" ]; then
 	useradd -m $USER
-elif [ $(uname) == "Darwin" ]; then
+elif [ $(uname) = "Darwin" ]; then
 	$SUDO sysadminctl -addUser $USER
 else
 	echo "Unsupported OS (don't know how to create a user)."

From 69ca37b6a25bcca5de158a7a0606bca83c36ba8e Mon Sep 17 00:00:00 2001
From: Aymeric Wibo <obiwac@gmail.com>
Date: Sun, 24 Nov 2024 15:51:01 +0100
Subject: [PATCH 12/22] tests: Export nothing for `$SUDO` if already root

---
 tests.sh | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/tests.sh b/tests.sh
index 6a5541bd..1a541e21 100644
--- a/tests.sh
+++ b/tests.sh
@@ -5,7 +5,9 @@ export TEST_OUT=.test-out
 
 # Find doas or sudo.
 
-if [ $(uname) = "Linux" ]; then
+if [ $(id -u) = 0 ]; then
+	export SUDO=
+elif [ $(uname) = "Linux" ]; then
 	# XXX Linux is annoying, 'which -s' doesn't exist.
 	export SUDO=sudo
 elif which -s doas; then

From da9a14ade43463c06ff0881bdd8da06274a408e7 Mon Sep 17 00:00:00 2001
From: Aymeric Wibo <obiwac@gmail.com>
Date: Sun, 24 Nov 2024 15:54:21 +0100
Subject: [PATCH 13/22] tests/install: Install using bootstrapped bob,
 otherwise we're copying over to the binary we're using

---
 tests/install.sh | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/tests/install.sh b/tests/install.sh
index 867a267d..bea5f6be 100644
--- a/tests/install.sh
+++ b/tests/install.sh
@@ -3,7 +3,7 @@ set -e
 
 # Test installing programs.
 
-$SUDO bob install
+$SUDO .bootstrap/bob install
 bob build
 
 [ -d /usr/local/share/bob/skeletons ]

From 1431d0678f8752c0ab1cb7ed3bcd459aab29404a Mon Sep 17 00:00:00 2001
From: Aymeric Wibo <obiwac@gmail.com>
Date: Sun, 24 Nov 2024 18:22:56 +0100
Subject: [PATCH 14/22] install: Move prefix assertion to after null-check, so
 it doesn't trigger when the install map is not set

---
 src/install.c | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/src/install.c b/src/install.c
index 24c3d69a..d4503b91 100644
--- a/src/install.c
+++ b/src/install.c
@@ -14,7 +14,7 @@
 #include <libgen.h>
 
 static flamingo_val_t* install_map = NULL;
-static char const* prefix;
+static char const* prefix = NULL;
 
 int setup_install_map(flamingo_t* flamingo, char const* _prefix) {
 	// Find the install map.
@@ -183,12 +183,12 @@ static int install_single(flamingo_val_t* key_val, char* val, bool installing_co
 }
 
 int install_all(char const* _prefix) {
-	assert(_prefix == prefix);
-
 	if (install_map == NULL || prefix == NULL) {
 		return 0;
 	}
 
+	assert(_prefix == prefix);
+
 	for (size_t i = 0; i < install_map->map.count; i++) {
 		flamingo_val_t* const val_val = install_map->map.vals[i];
 		char* const val = strndup(val_val->str.str, val_val->str.size);

From 9dfc2db8e565299c55f994e74f901290b5ba07a9 Mon Sep 17 00:00:00 2001
From: Aymeric Wibo <obiwac@gmail.com>
Date: Sun, 24 Nov 2024 18:23:35 +0100
Subject: [PATCH 15/22] tests: Pipe errors to `stderr`

---
 tests.sh       |  6 +++---
 tests/chown.sh | 13 +++++++++----
 2 files changed, 12 insertions(+), 7 deletions(-)

diff --git a/tests.sh b/tests.sh
index 1a541e21..cbfd2f2d 100644
--- a/tests.sh
+++ b/tests.sh
@@ -1,6 +1,6 @@
 #!/bin/sh
 
-export ASAN_OPTIONS=detect_leaks=0 # XXX For now, let's not worry about leaks.
+export ASAN_OPTIONS="detect_leaks=0" # XXX For now, let's not worry about leaks.
 export TEST_OUT=.test-out
 
 # Find doas or sudo.
@@ -15,7 +15,7 @@ elif which -s doas; then
 elif which -s sudo; then
 	export SUDO=sudo
 else
-	echo "No sudo or doas found."
+	echo "No sudo or doas found." >&2
 	exit 1
 fi
 
@@ -48,7 +48,7 @@ for test in $(ls -p tests | grep -v /); do
 done
 
 if [ $all_passed = 0 ]; then
-	echo "TESTS FAILED!"
+	echo "TESTS FAILED!" >&2
 	exit 1
 else
 	echo "ALL TESTS PASSED!"
diff --git a/tests/chown.sh b/tests/chown.sh
index e28040c2..e06d4526 100644
--- a/tests/chown.sh
+++ b/tests/chown.sh
@@ -19,9 +19,14 @@ USER=bobtestuser
 if [ $(uname) = "Linux" ] || [ $(uname) = "FreeBSD" ]; then
 	useradd -m $USER
 elif [ $(uname) = "Darwin" ]; then
-	$SUDO sysadminctl -addUser $USER
+	out=$($SUDO sysadminctl -addUser $USER 2>&1)
+
+	if [ $? != 0 ]; then
+		echo "Failed to create user: $out" >&2
+		exit 1
+	fi
 else
-	echo "Unsupported OS (don't know how to create a user)."
+	echo "Unsupported OS (don't know how to create a user)." >&2
 	exit 1
 fi
 
@@ -31,7 +36,7 @@ $SUDO chown -R bobtestuser tests/chown
 $SUDO bob -C tests/chown build
 
 if [ ! -z "$(find tests/chown ! -user bobtestuser)" ]; then
-	echo "Some files are not owned by bobtestuser in tests/chown."
+	echo "Some files are not owned by bobtestuser in tests/chown." >&2
 	exit 1
 fi
 
@@ -42,6 +47,6 @@ $SUDO bob -C tests/chown install
 owner=$(ls -ld /usr/local/share/bob_chown_test.fl | awk '{print $3}')
 
 if [ $owner != "root" ]; then
-	echo "Installed files are not owned by root."
+	echo "Installed files are not owned by root." >&2
 	exit 1
 fi

From e057c8d74859227fe9b0bf1caa5d5da0fd4ceeef Mon Sep 17 00:00:00 2001
From: Aymeric Wibo <obiwac@gmail.com>
Date: Sun, 24 Nov 2024 18:24:06 +0100
Subject: [PATCH 16/22] tests: Run path traversal order regression test, for
 #71

---
 tests/run_path_traversal_order.sh             | 31 +++++++++++++++++++
 .../bin1/test_command                         |  2 ++
 .../bin2/test_command                         |  2 ++
 tests/run_path_traversal_order/build.fl       |  6 ++++
 4 files changed, 41 insertions(+)
 create mode 100644 tests/run_path_traversal_order.sh
 create mode 100755 tests/run_path_traversal_order/bin1/test_command
 create mode 100755 tests/run_path_traversal_order/bin2/test_command
 create mode 100644 tests/run_path_traversal_order/build.fl

diff --git a/tests/run_path_traversal_order.sh b/tests/run_path_traversal_order.sh
new file mode 100644
index 00000000..7576df8a
--- /dev/null
+++ b/tests/run_path_traversal_order.sh
@@ -0,0 +1,31 @@
+#!/bin/sh
+
+# Regression test for the following PR:
+# https://github.com/inobulles/bob/pull/71
+
+out=$(bob -C tests/run_path_traversal_order run 2>/dev/null)
+
+if [ $? == 0 ]; then
+	echo "\$PATH was not set in order to find command; running should not have succeeded: $out" >&2
+	exit 1
+fi
+
+bin1=$(realpath tests/run_path_traversal_order/bin1)
+bin2=$(realpath tests/run_path_traversal_order/bin2)
+
+ORIG_PATH=$PATH
+export PATH="$bin1:$bin2:$ORIG_PATH"
+out=$(bob -C tests/run_path_traversal_order run 2>/dev/null)
+
+if ! [ "$out" = "bin1" ]; then
+	echo "Ran binary from the wrong path or some other issue occurred: $out" >&2
+	exit 1
+fi
+
+export PATH="$bin2:$bin1:$ORIG_PATH"
+out=$(bob -C tests/run_path_traversal_order run 2>/dev/null)
+
+if ! [ "$out" = "bin2" ]; then
+	echo "Ran binary from the wrong path or some other issue occurred: $out" >&2
+	exit 1
+fi
diff --git a/tests/run_path_traversal_order/bin1/test_command b/tests/run_path_traversal_order/bin1/test_command
new file mode 100755
index 00000000..b1e3c8e0
--- /dev/null
+++ b/tests/run_path_traversal_order/bin1/test_command
@@ -0,0 +1,2 @@
+#!/bin/sh
+echo "bin1"
diff --git a/tests/run_path_traversal_order/bin2/test_command b/tests/run_path_traversal_order/bin2/test_command
new file mode 100755
index 00000000..c58c57b2
--- /dev/null
+++ b/tests/run_path_traversal_order/bin2/test_command
@@ -0,0 +1,2 @@
+#!/bin/sh
+echo "bin2"
diff --git a/tests/run_path_traversal_order/build.fl b/tests/run_path_traversal_order/build.fl
new file mode 100644
index 00000000..0e353d02
--- /dev/null
+++ b/tests/run_path_traversal_order/build.fl
@@ -0,0 +1,6 @@
+# SPDX-License-Identifier: MIT
+# Copyright (c) 2024 Aymeric Wibo
+
+import bob
+
+run = ["test_command"]

From a5151879a19ed8979b9768e383ba450798ad8970 Mon Sep 17 00:00:00 2001
From: Aymeric Wibo <obiwac@gmail.com>
Date: Sun, 24 Nov 2024 18:26:54 +0100
Subject: [PATCH 17/22] ci: Don't bootstrap, `tests.sh` does this for us

---
 .cirrus.yml                 | 6 ------
 .github/workflows/tests.yml | 8 ++------
 tests.sh                    | 5 +++--
 3 files changed, 5 insertions(+), 14 deletions(-)

diff --git a/.cirrus.yml b/.cirrus.yml
index ed6f83d2..dad491b7 100644
--- a/.cirrus.yml
+++ b/.cirrus.yml
@@ -3,8 +3,6 @@ task:
   timeout_in: 5m
   container:
     image: gcc:latest
-  bootstrap_script:
-    - sh bootstrap.sh
   tests_script:
     - sh tests.sh
   amd64_artifacts:
@@ -15,8 +13,6 @@ task:
   timeout_in: 5m
   arm_container:
     image: gcc:latest
-  bootstrap_script:
-    - sh bootstrap.sh
   tests_script:
     - sh tests.sh
   arm64_artifacts:
@@ -32,8 +28,6 @@ compute_engine_instance:
 task:
   name: Test Bob with Clang on FreeBSD (x86_64)
   timeout_in: 30m
-  bootstrap_script:
-    - sh bootstrap.sh
   tests_script:
     - sh tests.sh
   amd64_artifacts:
diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
index d5f947b6..a021b8ad 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -14,9 +14,7 @@ jobs:
         with:
           version: latest
           platform: x64
-      - name: Bootstrap
-        run: sh bootstrap.sh
-      - name: Tests
+      - name: Run tests
         run: sh tests.sh
       - name: Set up tmate session
         if: ${{ failure() }}
@@ -35,9 +33,7 @@ jobs:
       ASAN_OPTIONS: detect_leaks=0
     steps:
       - uses: actions/checkout@v4
-      - name: Bootstrap
-        run: sh bootstrap.sh
-      - name: Tests
+      - name: Run tests
         run: sh tests.sh
       - name: Set up tmate session
         if: ${{ failure() }}
diff --git a/tests.sh b/tests.sh
index cbfd2f2d..1c176cfa 100644
--- a/tests.sh
+++ b/tests.sh
@@ -19,9 +19,10 @@ else
 	exit 1
 fi
 
-# Install Bob.
-
+echo "Bootstrapping Bob..."
 sh bootstrap.sh
+
+echo "Installing Bob..."
 $SUDO .bootstrap/bob install > /dev/null
 
 # Actually run the tests.

From 2b57720f236e667a82826cf9fa057e680908ff43 Mon Sep 17 00:00:00 2001
From: Aymeric Wibo <obiwac@gmail.com>
Date: Sun, 24 Nov 2024 18:29:00 +0100
Subject: [PATCH 18/22] tests: Keep environment variables when running `sudo`

Because otherwise `ASAN_OPTIONS` wouldn't be passed on.
---
 .github/workflows/tests.yml | 4 ----
 tests.sh                    | 6 +++---
 2 files changed, 3 insertions(+), 7 deletions(-)

diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
index a021b8ad..c0047220 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -5,8 +5,6 @@ on: [push, workflow_dispatch]
 jobs:
   run_tests:
     runs-on: ubuntu-latest
-    env:
-      ASAN_OPTIONS: detect_leaks=0
     steps:
       - uses: actions/checkout@v4
       - name: Set up compiler
@@ -29,8 +27,6 @@ jobs:
       matrix:
         os: [macos-13, macos-latest] # 'macos-13' is Intel, 'macos-latest' is M1.
     runs-on: ${{ matrix.os }}
-    env:
-      ASAN_OPTIONS: detect_leaks=0
     steps:
       - uses: actions/checkout@v4
       - name: Run tests
diff --git a/tests.sh b/tests.sh
index 1c176cfa..93a7c304 100644
--- a/tests.sh
+++ b/tests.sh
@@ -1,6 +1,6 @@
 #!/bin/sh
 
-export ASAN_OPTIONS="detect_leaks=0" # XXX For now, let's not worry about leaks.
+export ASAN_OPTIONS=detect_leaks=0 # XXX For now, let's not worry about leaks.
 export TEST_OUT=.test-out
 
 # Find doas or sudo.
@@ -9,11 +9,11 @@ if [ $(id -u) = 0 ]; then
 	export SUDO=
 elif [ $(uname) = "Linux" ]; then
 	# XXX Linux is annoying, 'which -s' doesn't exist.
-	export SUDO=sudo
+	export SUDO=sudo -E
 elif which -s doas; then
 	export SUDO=doas
 elif which -s sudo; then
-	export SUDO=sudo
+	export SUDO=sudo -E
 else
 	echo "No sudo or doas found." >&2
 	exit 1

From c50635120834c97a5e9136e5af92d05ecb20fb21 Mon Sep 17 00:00:00 2001
From: Aymeric Wibo <obiwac@gmail.com>
Date: Sun, 24 Nov 2024 18:36:36 +0100
Subject: [PATCH 19/22] tests: Bring `SUDO` out as an alias

---
 tests.sh                          | 24 +++++++-----------------
 tests/bob.sh                      |  2 ++
 tests/chown.sh                    | 12 +++++++-----
 tests/common.sh                   | 17 +++++++++++++++++
 tests/install.sh                  |  4 +++-
 tests/run_path_traversal_order.sh |  2 ++
 6 files changed, 38 insertions(+), 23 deletions(-)
 create mode 100644 tests/common.sh

diff --git a/tests.sh b/tests.sh
index 93a7c304..3f3ad887 100644
--- a/tests.sh
+++ b/tests.sh
@@ -1,35 +1,25 @@
 #!/bin/sh
 
+. tests/common.sh
+
 export ASAN_OPTIONS=detect_leaks=0 # XXX For now, let's not worry about leaks.
 export TEST_OUT=.test-out
 
-# Find doas or sudo.
-
-if [ $(id -u) = 0 ]; then
-	export SUDO=
-elif [ $(uname) = "Linux" ]; then
-	# XXX Linux is annoying, 'which -s' doesn't exist.
-	export SUDO=sudo -E
-elif which -s doas; then
-	export SUDO=doas
-elif which -s sudo; then
-	export SUDO=sudo -E
-else
-	echo "No sudo or doas found." >&2
-	exit 1
-fi
-
 echo "Bootstrapping Bob..."
 sh bootstrap.sh
 
 echo "Installing Bob..."
-$SUDO .bootstrap/bob install > /dev/null
+SUDO .bootstrap/bob install > /dev/null
 
 # Actually run the tests.
 
 all_passed=1
 
 for test in $(ls -p tests | grep -v /); do
+	if [ $test = "common.sh" ]; then
+		continue
+	fi
+
 	if [ -d $test ]; then
 		continue
 	fi
diff --git a/tests/bob.sh b/tests/bob.sh
index 725493b9..534073ed 100644
--- a/tests/bob.sh
+++ b/tests/bob.sh
@@ -1,6 +1,8 @@
 #!/bin/sh
 set -e
 
+. tests/common.sh
+
 # Test building Bob itself.
 
 bob run -o $TEST_OUT/test-bob build
diff --git a/tests/chown.sh b/tests/chown.sh
index e06d4526..76732e49 100644
--- a/tests/chown.sh
+++ b/tests/chown.sh
@@ -1,6 +1,8 @@
 #!/bin/sh
 set -e
 
+. tests/common.sh
+
 # Regression test for the following PR:
 # https://github.com/inobulles/bob/pull/69
 # Basically, there's this feature which chown's generated files to the owner of the project directory when running Bob as root.
@@ -10,7 +12,7 @@ set -e
 # Make sure everything is clean.
 
 INSTALL_PATH=/usr/local/share/bob_chown_test.fl
-$SUDO rm -rf tests/chown/.bob $INSTALL_PATH
+SUDO rm -rf tests/chown/.bob $INSTALL_PATH
 
 # Create a user.
 
@@ -19,7 +21,7 @@ USER=bobtestuser
 if [ $(uname) = "Linux" ] || [ $(uname) = "FreeBSD" ]; then
 	useradd -m $USER
 elif [ $(uname) = "Darwin" ]; then
-	out=$($SUDO sysadminctl -addUser $USER 2>&1)
+	out=$(SUDO sysadminctl -addUser $USER 2>&1)
 
 	if [ $? != 0 ]; then
 		echo "Failed to create user: $out" >&2
@@ -32,15 +34,15 @@ fi
 
 # Chown the project directory to that user.
 
-$SUDO chown -R bobtestuser tests/chown
-$SUDO bob -C tests/chown build
+SUDO chown -R bobtestuser tests/chown
+SUDO bob -C tests/chown build
 
 if [ ! -z "$(find tests/chown ! -user bobtestuser)" ]; then
 	echo "Some files are not owned by bobtestuser in tests/chown." >&2
 	exit 1
 fi
 
-$SUDO bob -C tests/chown install
+SUDO bob -C tests/chown install
 
 # XXX Annoyingly, 'stat -f %Su' doesn't work on Linux (you have to use 'stat -c %U').
 
diff --git a/tests/common.sh b/tests/common.sh
new file mode 100644
index 00000000..2ca5455f
--- /dev/null
+++ b/tests/common.sh
@@ -0,0 +1,17 @@
+#!/bin/sh
+
+# Find doas or sudo.
+
+if [ $(id -u) = 0 ]; then
+	alias SUDO=""
+elif [ $(uname) = "Linux" ]; then
+	# XXX Linux is annoying, 'which -s' doesn't exist.
+	alias SUDO="sudo -E"
+elif which -s doas; then
+	alias SUDO="doas"
+elif which -s sudo; then
+	alias SUDO="sudo -E"
+else
+	echo "No sudo or doas found." >&2
+	exit 1
+fi
diff --git a/tests/install.sh b/tests/install.sh
index bea5f6be..174fad0e 100644
--- a/tests/install.sh
+++ b/tests/install.sh
@@ -1,9 +1,11 @@
 #!/bin/sh
 set -e
 
+. tests/common.sh
+
 # Test installing programs.
 
-$SUDO .bootstrap/bob install
+SUDO .bootstrap/bob install
 bob build
 
 [ -d /usr/local/share/bob/skeletons ]
diff --git a/tests/run_path_traversal_order.sh b/tests/run_path_traversal_order.sh
index 7576df8a..d69875da 100644
--- a/tests/run_path_traversal_order.sh
+++ b/tests/run_path_traversal_order.sh
@@ -1,5 +1,7 @@
 #!/bin/sh
 
+. tests/common.sh
+
 # Regression test for the following PR:
 # https://github.com/inobulles/bob/pull/71
 

From f1cecb81e2f7906aa13b644595b4309360367b76 Mon Sep 17 00:00:00 2001
From: Aymeric Wibo <obiwac@gmail.com>
Date: Sun, 24 Nov 2024 18:38:40 +0100
Subject: [PATCH 20/22] tests: Couple minor fixes

---
 tests/chown.sh                    | 2 +-
 tests/run_path_traversal_order.sh | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/tests/chown.sh b/tests/chown.sh
index 76732e49..5811ba42 100644
--- a/tests/chown.sh
+++ b/tests/chown.sh
@@ -19,7 +19,7 @@ SUDO rm -rf tests/chown/.bob $INSTALL_PATH
 USER=bobtestuser
 
 if [ $(uname) = "Linux" ] || [ $(uname) = "FreeBSD" ]; then
-	useradd -m $USER
+	SUDO useradd -m $USER
 elif [ $(uname) = "Darwin" ]; then
 	out=$(SUDO sysadminctl -addUser $USER 2>&1)
 
diff --git a/tests/run_path_traversal_order.sh b/tests/run_path_traversal_order.sh
index d69875da..77c50fed 100644
--- a/tests/run_path_traversal_order.sh
+++ b/tests/run_path_traversal_order.sh
@@ -7,7 +7,7 @@
 
 out=$(bob -C tests/run_path_traversal_order run 2>/dev/null)
 
-if [ $? == 0 ]; then
+if [ $? = 0 ]; then
 	echo "\$PATH was not set in order to find command; running should not have succeeded: $out" >&2
 	exit 1
 fi

From d900b1b18b9ea8aa82a9398525c2c0fe7df969e8 Mon Sep 17 00:00:00 2001
From: Aymeric Wibo <obiwac@gmail.com>
Date: Sun, 24 Nov 2024 18:50:14 +0100
Subject: [PATCH 21/22] tests/chown: Fix user creation on FreeBSD

---
 tests.sh        | 3 ---
 tests/chown.sh  | 5 +++--
 tests/common.sh | 3 +++
 3 files changed, 6 insertions(+), 5 deletions(-)

diff --git a/tests.sh b/tests.sh
index 3f3ad887..56e1bb44 100644
--- a/tests.sh
+++ b/tests.sh
@@ -2,9 +2,6 @@
 
 . tests/common.sh
 
-export ASAN_OPTIONS=detect_leaks=0 # XXX For now, let's not worry about leaks.
-export TEST_OUT=.test-out
-
 echo "Bootstrapping Bob..."
 sh bootstrap.sh
 
diff --git a/tests/chown.sh b/tests/chown.sh
index 5811ba42..a6511fd5 100644
--- a/tests/chown.sh
+++ b/tests/chown.sh
@@ -1,5 +1,4 @@
 #!/bin/sh
-set -e
 
 . tests/common.sh
 
@@ -18,7 +17,9 @@ SUDO rm -rf tests/chown/.bob $INSTALL_PATH
 
 USER=bobtestuser
 
-if [ $(uname) = "Linux" ] || [ $(uname) = "FreeBSD" ]; then
+if [ $(uname) = "FreeBSD" ]; then
+	SUDO pw adduser -n $USER
+elif [ $(uname) = "Linux" ]; then
 	SUDO useradd -m $USER
 elif [ $(uname) = "Darwin" ]; then
 	out=$(SUDO sysadminctl -addUser $USER 2>&1)
diff --git a/tests/common.sh b/tests/common.sh
index 2ca5455f..8c6b5920 100644
--- a/tests/common.sh
+++ b/tests/common.sh
@@ -1,5 +1,8 @@
 #!/bin/sh
 
+export ASAN_OPTIONS=detect_leaks=0 # XXX For now, let's not worry about leaks.
+export TEST_OUT=.test-out
+
 # Find doas or sudo.
 
 if [ $(id -u) = 0 ]; then

From 8848d61826ba418687720d069bb75a8f0dd5dde7 Mon Sep 17 00:00:00 2001
From: Aymeric Wibo <obiwac@gmail.com>
Date: Sun, 24 Nov 2024 19:52:58 +0100
Subject: [PATCH 22/22] tests: Add frugality regression test, for #70

---
 src/class/cc.c           |   2 +-
 src/frugal.c             |   3 +
 tests/frugality.sh       | 174 +++++++++++++++++++++++++++++++++++++++
 tests/frugality/build.fl |  13 +++
 tests/frugality/src1.c   |   5 ++
 tests/frugality/src2.c   |  10 +++
 tests/frugality/src2.h   |   1 +
 7 files changed, 207 insertions(+), 1 deletion(-)
 create mode 100644 tests/frugality.sh
 create mode 100644 tests/frugality/build.fl
 create mode 100644 tests/frugality/src1.c
 create mode 100644 tests/frugality/src2.c
 create mode 100644 tests/frugality/src2.h

diff --git a/src/class/cc.c b/src/class/cc.c
index e0451697..efd78a0c 100644
--- a/src/class/cc.c
+++ b/src/class/cc.c
@@ -60,7 +60,7 @@ static void get_include_deps(compile_task_t* task, char* cc) {
 	// -MT "": Exclude the source file from the output.
 	// TODO Can we do this in parallel easily with 'cmd_exec_async'?
 
-	cmd_t __attribute__((cleanup(cmd_free))) cmd;
+	cmd_t __attribute__((cleanup(cmd_free))) cmd = {0};
 	cmd_create(&cmd, cc, "-fdiagnostics-color=always", "-MM", "-MT", "", task->src, NULL);
 	add_flags(&cmd, task);
 
diff --git a/src/frugal.c b/src/frugal.c
index 4a5bec74..05d5120a 100644
--- a/src/frugal.c
+++ b/src/frugal.c
@@ -59,6 +59,9 @@ bool frugal_flags(flamingo_val_t* flags, char* out) {
 
 	fclose(f);
 
+	// Note that we want to compare the order of these flags too, so just compare the strings.
+	// There could be some cases where flipping the order of flags could change the build, which is perhaps rare but let's not try to be too smart here.
+
 	if (strcmp(prev_flag_str, flag_str) != 0) {
 		goto write_out;
 	}
diff --git a/tests/frugality.sh b/tests/frugality.sh
new file mode 100644
index 00000000..3a86a80f
--- /dev/null
+++ b/tests/frugality.sh
@@ -0,0 +1,174 @@
+#!/bin/sh
+
+. tests/common.sh
+
+# Regression test for the following PR:
+# https://github.com/inobulles/bob/pull/70
+
+# Make sure build.fl's Cc flags are correct.
+
+if [ $(uname) = Linux ]; then # XXX Linux is annoying.
+	sed -i 's/-std=c11/-std=c99/g' tests/frugality/build.fl
+else
+	sed -i '' 's/-std=c11/-std=c99/g' tests/frugality/build.fl
+fi
+
+# Clean up.
+
+BOB_PATH=tests/frugality/.bob
+rm -rf $BOB_PATH
+
+# Functions for getting the mtimes of all cookies and preinstalled files.
+
+build() {
+	out=$(bob -C tests/frugality run 2>&1)
+
+	if [ $? != 0 ]; then
+		echo "Build failed: $out" >&2
+		exit 1
+	fi
+}
+
+get_mtimes() {
+	export src1_${1}mtime=$(date -r $BOB_PATH/bob/src1.c.cookie.6531c664ecf.o +%s)
+	export src2_${1}mtime=$(date -r $BOB_PATH/bob/src2.c.cookie.6531c665310.o +%s)
+	export linked_${1}mtime=$(date -r $BOB_PATH/bob/linker.link.cookie.33faaddbf5af8a68.l +%s)
+
+	export obj1_${1}mtime=$(date -r $BOB_PATH/prefix/obj1.o +%s)
+	export obj2_${1}mtime=$(date -r $BOB_PATH/prefix/obj2.o +%s)
+	export bin_${1}mtime=$(date -r $BOB_PATH/prefix/bin +%s)
+}
+
+update() {
+	# We're running here instead of just building (even if there's nothing to run).
+	# This is because we want the preinstallation step to run.
+
+	build
+	get_mtimes ""
+
+	# If the mtime of the installed file is less than that of the cookie, then it was not reinstalled, which is bad!.
+
+	if [ $obj1_mtime -lt $src1_mtime ]; then
+		echo "obj1.o's cookie was updated, but it was not preinstalled." >&2
+		exit 1
+	fi
+
+	if [ $obj2_mtime -lt $src2_mtime ]; then
+		echo "obj2.o's cookie was updated, but it was not preinstalled." >&2
+		exit 1
+	fi
+
+	if [ $bin_mtime -lt $linked_mtime ]; then
+		echo "bin's cookie was updated, but it was not preinstalled." >&2
+		exit 1
+	fi
+
+	# If the cookie was not updated, then it shouldn't have been preinstalled.
+
+	export src1_updated=
+	export src2_updated=
+	export linked_updated=
+
+	if [ $src1_mtime = $src1_prev_mtime ]; then
+		if [ $obj1_mtime -gt $obj1_prev_mtime ]; then
+			echo "obj1.o's cookie was not updated, but it was preinstalled." >&2
+			exit 1
+		fi
+	elif [ $src1_mtime -gt $src1_prev_mtime ]; then
+		src1_updated=true
+	else
+		echo "This is impossible." >&2
+		exit 1
+	fi
+
+	if [ $src2_mtime = $src2_prev_mtime ]; then
+		if [ $obj2_mtime -gt $obj2_prev_mtime ]; then
+			echo "obj2.o's cookie was not updated, but it was preinstalled." >&2
+			exit 1
+		fi
+	elif [ $src2_mtime -gt $src2_prev_mtime ]; then
+		src2_updated=true
+	else
+		echo "This is impossible." >&2
+		exit 1
+	fi
+
+	if [ $linked_mtime = $linked_prev_mtime ]; then
+		if [ $bin_mtime -gt $bin_prev_mtime ]; then
+			echo "bin's cookie was not updated, but it was preinstalled." >&2
+			exit 1
+		fi
+	elif [ $linked_mtime -gt $linked_prev_mtime ]; then
+		linked_updated=true
+	else
+		echo "This is impossible." >&2
+		exit 1
+	fi
+}
+
+build # Run this once to populate .bob already.
+
+echo "Test 1: changing 'src2.c'."
+
+get_mtimes "prev_"
+sleep 1
+touch tests/frugality/src2.c
+update
+
+if [ "$src1_updated" = true ] || [ -z $src2_updated ] || [ -z $linked_updated ]; then
+	echo "Changing 'src2.c' failed." >&2
+	exit 1
+fi
+
+echo "Test 2: changing both 'src1.c' and 'src2.c'."
+
+get_mtimes "prev_"
+sleep 1
+touch tests/frugality/src1.c tests/frugality/src2.c
+update
+
+if [ -z $src1_updated ] || [ -z $src2_updated ] || [ -z $linked_updated ]; then
+	echo "Changing 'src1.c' and 'src2.c' failed." >&2
+	exit 1
+fi
+
+echo "Test 3: changing nothing."
+
+get_mtimes "prev_"
+sleep 1
+update
+
+if [ $src1_updated ] || [ $src2_updated ] || [ $linked_updated ]; then
+	echo "Changing nothing failed." >&2
+	exit 1
+fi
+
+echo "Test 4: changing 'src2.h'."
+
+get_mtimes "prev_"
+sleep 1
+touch tests/frugality/src2.h
+update
+
+if [ $src1_updated ] || [ -z $src2_updated ] || [ -z $linked_updated ]; then
+	echo "Changing 'src2.h' failed." >&2
+	exit 1
+fi
+
+echo "Test 5: changing compilation flags for all source files."
+
+get_mtimes "prev_"
+sleep 1
+
+if [ $(uname) = Linux ]; then # XXX Linux is annoying.
+	sed -i 's/-std=c99/-std=c11/g' tests/frugality/build.fl
+else
+	sed -i '' 's/-std=c99/-std=c11/g' tests/frugality/build.fl
+fi
+
+update
+
+if [ -z $src1_updated ] || [ -z $src2_updated ] || [ -z $linked_updated ]; then
+	echo "Changing compilation flags failed." >&2
+	exit 1
+fi
diff --git a/tests/frugality/build.fl b/tests/frugality/build.fl
new file mode 100644
index 00000000..82fd8c26
--- /dev/null
+++ b/tests/frugality/build.fl
@@ -0,0 +1,13 @@
+import bob
+
+let src1 = "src1.c"
+let src2 = "src2.c"
+
+let obj = Cc(["-std=c11"]).compile([src1, src2])
+let linked = Linker([]).link(obj)
+
+install = {
+	obj[0]: "obj1.o",
+	obj[1]: "obj2.o",
+	linked: "bin",
+}
diff --git a/tests/frugality/src1.c b/tests/frugality/src1.c
new file mode 100644
index 00000000..f15123c5
--- /dev/null
+++ b/tests/frugality/src1.c
@@ -0,0 +1,5 @@
+#include <stdio.h>
+
+void fn(void) {
+	printf("B\n");
+}
diff --git a/tests/frugality/src2.c b/tests/frugality/src2.c
new file mode 100644
index 00000000..440a21ab
--- /dev/null
+++ b/tests/frugality/src2.c
@@ -0,0 +1,10 @@
+#include "src2.h"
+#include <stdio.h>
+
+void fn(void);
+
+int main(void) {
+	printf(STR "\n");
+	fn();
+	return 0;
+}
diff --git a/tests/frugality/src2.h b/tests/frugality/src2.h
new file mode 100644
index 00000000..e3703f37
--- /dev/null
+++ b/tests/frugality/src2.h
@@ -0,0 +1 @@
+#define STR "A"