Merge lp:~hazmat/pyjuju/hooks-with-formula-dir into lp:pyjuju
- hooks-with-formula-dir
- Merge into trunk
Status: | Merged | ||||
---|---|---|---|---|---|
Approved by: | Kapil Thangavelu | ||||
Approved revision: | 262 | ||||
Merged at revision: | 267 | ||||
Proposed branch: | lp:~hazmat/pyjuju/hooks-with-formula-dir | ||||
Merge into: | lp:pyjuju | ||||
Diff against target: |
271 lines (+82/-15) 6 files modified
docs/source/faq.rst (+7/-0) ensemble/hooks/invoker.py (+11/-2) ensemble/hooks/tests/test_executor.py (+9/-3) ensemble/hooks/tests/test_invoker.py (+46/-6) ensemble/unit/lifecycle.py (+5/-3) ensemble/unit/tests/test_lifecycle.py (+4/-1) |
||||
To merge this branch: | bzr merge lp:~hazmat/pyjuju/hooks-with-formula-dir | ||||
Related bugs: |
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
William Reade (community) | Approve | ||
Gustavo Niemeyer | Approve | ||
Jim Baker | Pending | ||
Review via email: mp+65557@code.launchpad.net |
Commit message
Description of the change
Hooks are now executed in the unit root directory, and have a FORMULA_DIR environment variable pointing to the formula directory.
Gustavo Niemeyer (niemeyer) wrote : | # |
Gustavo Niemeyer (niemeyer) wrote : | # |
Putting it to WIP for pondering about [1] and so that the second reviewer gets to see the result of it.
Kapil Thangavelu (hazmat) wrote : | # |
Excerpts from Gustavo Niemeyer's message of Wed Jul 06 00:51:26 UTC 2011:
> [1]
>
> We talked about having the formula path as the cwd, and I've seen at least one person mentioning the expectation that the cwd is actually formula/hooks. I'd probably be worth to do a quick individual questioning about what each formula author we have expects cwd to be, and use that instead of the unit dir.
from irc
<hazmat> a question for formula authors in the room, which directory would you want your hooks to run in.. The formula hooks directory, the formula directory, or the root of the unit directory (the formula for the unit lives at '/formula' under the unit root)?
<fwereade> what else lives in the unit root at the moment?
<fwereade> the formula directory feels natural to me but I am basically 100% ignorant
<hazmat> fwereade, with lxc, the unit root will be the fs root "/" of the container
<fwereade> ah ok
<hazmat> at the moment, there's just the service unit log file and the formula directory in the root
<fwereade> ok, the hooks directory feels wrong because... well, it's the formula's interface, and if we run in that dir we encourage people to clutter it up with other stuff
<fwereade> I don't think I have a context to judge the distinction between running in the formula dir and the unit root
<hazmat> fwereade, agreed, authors can include arbitrary library or configuration stuff in the formula, its probably nicer to address that from the root, then including a '..' everywhere or cluttering up the hooks directory
<hazmat> er... s/root/ i mean the formula directory
<fwereade> yep
* hazmat makes it so
- 262. By Kapil Thangavelu
-
merge the debug-with-
formula- dir, forget this was setup as a bzr-pipeline
Gustavo Niemeyer (niemeyer) wrote : | # |
This looks good, thanks!
Two details to ponder about before merging:
[2]
The variable name: should it be prefixed by ENSEMBLE_ like the rest of them? Is it more common to have _PATH rather than _DIR?
[3]
Would be good to document the directory next to the hooks description in the formulas spec.
Kapil Thangavelu (hazmat) wrote : | # |
hmm.. i actually meant to yank the formula dir env variable, but i had shelved it while resolving some test failures. i don't really see the need for it anymore since its the starting current dir of the executing hook. some hooks may want the env variable, but they can easily save cwd if they do.
Gustavo Niemeyer (niemeyer) wrote : | # |
Can we name it as ENSEMBLE_
I think it's still useful to have it around, for the purpose of tools. It's convenient to be able to call anything in a tool and have the tool knowing where to find things relative to the formula, independently from the CWD (e.g. templates).
William Reade (fwereade) wrote : | # |
Looks good to me(as do niemeyer's suggestions).
Preview Diff
1 | === modified file 'docs/source/faq.rst' | |||
2 | --- docs/source/faq.rst 2011-06-20 19:04:35 +0000 | |||
3 | +++ docs/source/faq.rst 2011-07-06 18:05:30 +0000 | |||
4 | @@ -59,6 +59,13 @@ | |||
5 | 59 | Also integration work with the `Orchestra <https://launchpad.net/orchestra>`_ | 59 | Also integration work with the `Orchestra <https://launchpad.net/orchestra>`_ |
6 | 60 | project is underway to enable deployment to hardware machines | 60 | project is underway to enable deployment to hardware machines |
7 | 61 | 61 | ||
8 | 62 | What directory are hooks executed in? | ||
9 | 63 | |||
10 | 64 | Hooks are executed in the formula directory (the parent directory to the hook | ||
11 | 65 | directory). This is primarily to encourage putting additional resources that | ||
12 | 66 | a hook may use outside of the hooks directory which is the public interface | ||
13 | 67 | of the formula. | ||
14 | 68 | |||
15 | 62 | How can I contact the Ensemble team? | 69 | How can I contact the Ensemble team? |
16 | 63 | 70 | ||
17 | 64 | User and formula author oriented resources | 71 | User and formula author oriented resources |
18 | 65 | 72 | ||
19 | === modified file 'ensemble/hooks/invoker.py' | |||
20 | --- ensemble/hooks/invoker.py 2011-06-11 02:25:26 +0000 | |||
21 | +++ ensemble/hooks/invoker.py 2011-07-06 18:05:30 +0000 | |||
22 | @@ -57,18 +57,24 @@ | |||
23 | 57 | `logger` -- instance of a logging.Logger object used to capture | 57 | `logger` -- instance of a logging.Logger object used to capture |
24 | 58 | hook output. | 58 | hook output. |
25 | 59 | """ | 59 | """ |
27 | 60 | def __init__(self, context, change, client_id, socket_path, logger): | 60 | def __init__(self, context, change, client_id, socket_path, |
28 | 61 | unit_path, logger): | ||
29 | 61 | self.environment = {} | 62 | self.environment = {} |
30 | 62 | self._context = context | 63 | self._context = context |
31 | 63 | self._change = change | 64 | self._change = change |
32 | 64 | self._client_id = client_id | 65 | self._client_id = client_id |
33 | 65 | self._socket_path = socket_path | 66 | self._socket_path = socket_path |
34 | 67 | self._unit_path = unit_path | ||
35 | 66 | self._log = logger | 68 | self._log = logger |
36 | 67 | # The twisted.internet.process.Process instance. | 69 | # The twisted.internet.process.Process instance. |
37 | 68 | self._process = None | 70 | self._process = None |
38 | 69 | # The hook executable path | 71 | # The hook executable path |
39 | 70 | self._process_executable = None | 72 | self._process_executable = None |
40 | 71 | 73 | ||
41 | 74 | @property | ||
42 | 75 | def unit_path(self): | ||
43 | 76 | return self._unit_path | ||
44 | 77 | |||
45 | 72 | def get_environment(self): | 78 | def get_environment(self): |
46 | 73 | """ | 79 | """ |
47 | 74 | Returns the environment used to run the hook as a dict. | 80 | Returns the environment used to run the hook as a dict. |
48 | @@ -78,6 +84,7 @@ | |||
49 | 78 | """ | 84 | """ |
50 | 79 | base = dict(ENSEMBLE_AGENT_SOCKET=self._socket_path, | 85 | base = dict(ENSEMBLE_AGENT_SOCKET=self._socket_path, |
51 | 80 | ENSEMBLE_CLIENT_ID=self._client_id, | 86 | ENSEMBLE_CLIENT_ID=self._client_id, |
52 | 87 | FORMULA_DIR=os.path.join(self._unit_path, "formula"), | ||
53 | 81 | ENSEMBLE_UNIT_NAME=os.environ["ENSEMBLE_UNIT_NAME"], | 88 | ENSEMBLE_UNIT_NAME=os.environ["ENSEMBLE_UNIT_NAME"], |
54 | 82 | PATH=os.environ["PATH"], | 89 | PATH=os.environ["PATH"], |
55 | 83 | PYTHON=os.environ.get("PYTHON", sys.executable), | 90 | PYTHON=os.environ.get("PYTHON", sys.executable), |
56 | @@ -158,7 +165,9 @@ | |||
57 | 158 | 165 | ||
58 | 159 | return result | 166 | return result |
59 | 160 | 167 | ||
61 | 161 | self._process = reactor.spawnProcess(hook_proto, hook, [hook], env) | 168 | self._process = reactor.spawnProcess( |
62 | 169 | hook_proto, hook, [hook], env, | ||
63 | 170 | os.path.join(self._unit_path, "formula")) | ||
64 | 162 | deferred.addBoth(cleanup_process) | 171 | deferred.addBoth(cleanup_process) |
65 | 163 | 172 | ||
66 | 164 | return deferred | 173 | return deferred |
67 | 165 | 174 | ||
68 | === modified file 'ensemble/hooks/tests/test_executor.py' | |||
69 | --- ensemble/hooks/tests/test_executor.py 2011-06-03 14:17:31 +0000 | |||
70 | +++ ensemble/hooks/tests/test_executor.py 2011-07-06 18:05:30 +0000 | |||
71 | @@ -3,7 +3,6 @@ | |||
72 | 3 | import subprocess | 3 | import subprocess |
73 | 4 | 4 | ||
74 | 5 | from twisted.internet.defer import inlineCallbacks, Deferred | 5 | from twisted.internet.defer import inlineCallbacks, Deferred |
75 | 6 | from twisted.internet.process import Process | ||
76 | 7 | from twisted.internet.error import ProcessExitedAlready | 6 | from twisted.internet.error import ProcessExitedAlready |
77 | 8 | 7 | ||
78 | 9 | import ensemble.hooks.executor | 8 | import ensemble.hooks.executor |
79 | @@ -192,23 +191,30 @@ | |||
80 | 192 | ensemble.hooks.executor, "DEBUG_HOOK_TEMPLATE", | 191 | ensemble.hooks.executor, "DEBUG_HOOK_TEMPLATE", |
81 | 193 | "\n".join(("#!/bin/bash", | 192 | "\n".join(("#!/bin/bash", |
82 | 194 | "exit_handler() {", | 193 | "exit_handler() {", |
84 | 195 | " echo clean exit %s", | 194 | " echo clean exit", |
85 | 196 | " exit 0", | 195 | " exit 0", |
86 | 197 | "}", | 196 | "}", |
87 | 198 | 'trap "exit_handler" HUP', | 197 | 'trap "exit_handler" HUP', |
88 | 199 | "sleep 0.2", | 198 | "sleep 0.2", |
89 | 200 | "exit 1"))) | 199 | "exit 1"))) |
90 | 201 | 200 | ||
91 | 201 | unit_dir = self.makeDir() | ||
92 | 202 | |||
93 | 203 | formula_dir = os.path.join(unit_dir, "formula") | ||
94 | 204 | self.makeDir(path=formula_dir) | ||
95 | 205 | |||
96 | 202 | self._executor.set_debug(["*"]) | 206 | self._executor.set_debug(["*"]) |
97 | 203 | log = logging.getLogger("invoker") | 207 | log = logging.getLogger("invoker") |
98 | 204 | # Populate environment variables for default invoker. | 208 | # Populate environment variables for default invoker. |
99 | 205 | self.change_environment( | 209 | self.change_environment( |
100 | 206 | ENSEMBLE_UNIT_NAME="dummy/1", PATH="/bin/:/usr/bin") | 210 | ENSEMBLE_UNIT_NAME="dummy/1", PATH="/bin/:/usr/bin") |
101 | 207 | output = self.capture_logging("invoker", level=logging.DEBUG) | 211 | output = self.capture_logging("invoker", level=logging.DEBUG) |
103 | 208 | invoker = Invoker(None, None, "constant", self.makeFile(), log) | 212 | invoker = Invoker( |
104 | 213 | None, None, "constant", self.makeFile(), unit_dir, log) | ||
105 | 209 | 214 | ||
106 | 210 | self._executor.start() | 215 | self._executor.start() |
107 | 211 | hook_done = self._executor(invoker, "abc") | 216 | hook_done = self._executor(invoker, "abc") |
108 | 217 | |||
109 | 212 | # Give a moment for execution to start. | 218 | # Give a moment for execution to start. |
110 | 213 | yield self.sleep(0.1) | 219 | yield self.sleep(0.1) |
111 | 214 | self._executor.set_debug(None) | 220 | self._executor.set_debug(None) |
112 | 215 | 221 | ||
113 | === modified file 'ensemble/hooks/tests/test_invoker.py' | |||
114 | --- ensemble/hooks/tests/test_invoker.py 2011-06-11 02:37:35 +0000 | |||
115 | +++ ensemble/hooks/tests/test_invoker.py 2011-07-06 18:05:30 +0000 | |||
116 | @@ -8,7 +8,6 @@ | |||
117 | 8 | import yaml | 8 | import yaml |
118 | 9 | 9 | ||
119 | 10 | from twisted.internet import defer | 10 | from twisted.internet import defer |
120 | 11 | from twisted.python.log import PythonLoggingObserver | ||
121 | 12 | 11 | ||
122 | 13 | import ensemble | 12 | import ensemble |
123 | 14 | from ensemble import errors | 13 | from ensemble import errors |
124 | @@ -26,9 +25,10 @@ | |||
125 | 26 | class MockUnitAgent(object): | 25 | class MockUnitAgent(object): |
126 | 27 | """Pretends to implement the client state cache, and the UA hook socket. | 26 | """Pretends to implement the client state cache, and the UA hook socket. |
127 | 28 | """ | 27 | """ |
129 | 29 | def __init__(self, client, socket_path): | 28 | def __init__(self, client, socket_path, formula_dir): |
130 | 30 | self.client = client | 29 | self.client = client |
131 | 31 | self.socket_path = socket_path | 30 | self.socket_path = socket_path |
132 | 31 | self.formula_dir = formula_dir | ||
133 | 32 | self._clients = {} # client_id -> HookContext | 32 | self._clients = {} # client_id -> HookContext |
134 | 33 | 33 | ||
135 | 34 | self._agent_log = logging.getLogger("unit-agent") | 34 | self._agent_log = logging.getLogger("unit-agent") |
136 | @@ -74,6 +74,7 @@ | |||
137 | 74 | exe = invoker.Invoker(context, change, | 74 | exe = invoker.Invoker(context, change, |
138 | 75 | self.get_client_id(), | 75 | self.get_client_id(), |
139 | 76 | self.socket_path, | 76 | self.socket_path, |
140 | 77 | self.formula_dir, | ||
141 | 77 | logger) | 78 | logger) |
142 | 78 | return exe | 79 | return exe |
143 | 79 | 80 | ||
144 | @@ -185,7 +186,12 @@ | |||
145 | 185 | 186 | ||
146 | 186 | self.update_invoker_env("mysql/0", "wordpress/0") | 187 | self.update_invoker_env("mysql/0", "wordpress/0") |
147 | 187 | self.socket_path = self.makeFile() | 188 | self.socket_path = self.makeFile() |
149 | 188 | self.ua = MockUnitAgent(self.client, self.socket_path) | 189 | unit_dir = self.makeDir() |
150 | 190 | self.makeDir(path=os.path.join(unit_dir, "formula")) | ||
151 | 191 | self.ua = MockUnitAgent( | ||
152 | 192 | self.client, | ||
153 | 193 | self.socket_path, | ||
154 | 194 | unit_dir) | ||
155 | 189 | 195 | ||
156 | 190 | @defer.inlineCallbacks | 196 | @defer.inlineCallbacks |
157 | 191 | def tearDown(self): | 197 | def tearDown(self): |
158 | @@ -279,9 +285,38 @@ | |||
159 | 279 | self.assertEqual(data["forgotten"], "lyrics") | 285 | self.assertEqual(data["forgotten"], "lyrics") |
160 | 280 | 286 | ||
161 | 281 | @defer.inlineCallbacks | 287 | @defer.inlineCallbacks |
162 | 288 | def test_hook_exec_in_formula_directory(self): | ||
163 | 289 | """Hooks are executed in the formula directory.""" | ||
164 | 290 | yield self.build_default_relationships() | ||
165 | 291 | hook_log = self.capture_logging("hook") | ||
166 | 292 | exe = self.ua.get_invoker("db", "add", "mysql/0", self.mysql_relation, | ||
167 | 293 | client_id="client_id") | ||
168 | 294 | self.assertTrue(os.path.isdir(exe.unit_path)) | ||
169 | 295 | exe.environment["ENSEMBLE_REMOTE_UNIT"] = "wordpress/0" | ||
170 | 296 | |||
171 | 297 | # verify the hook's execution directory | ||
172 | 298 | hook_path = self.makeFile("#!/bin/bash\necho $PWD") | ||
173 | 299 | os.chmod(hook_path, stat.S_IEXEC | stat.S_IREAD) | ||
174 | 300 | result = yield exe(hook_path) | ||
175 | 301 | self.assertEqual(hook_log.getvalue().strip(), | ||
176 | 302 | os.path.join(exe.unit_path, "formula")) | ||
177 | 303 | self.assertEqual(result, 0) | ||
178 | 304 | |||
179 | 305 | # Reset the output capture | ||
180 | 306 | hook_log.seek(0) | ||
181 | 307 | hook_log.truncate() | ||
182 | 308 | |||
183 | 309 | # Verify the environment variable is set. | ||
184 | 310 | hook_path = self.makeFile("#!/bin/bash\necho $FORMULA_DIR") | ||
185 | 311 | os.chmod(hook_path, stat.S_IEXEC | stat.S_IREAD) | ||
186 | 312 | result = yield exe(hook_path) | ||
187 | 313 | self.assertEqual(hook_log.getvalue().strip(), | ||
188 | 314 | os.path.join(exe.unit_path, "formula")) | ||
189 | 315 | |||
190 | 316 | @defer.inlineCallbacks | ||
191 | 282 | def test_spawn_cli_config_get(self): | 317 | def test_spawn_cli_config_get(self): |
192 | 283 | """Validate that config-get returns expected values.""" | 318 | """Validate that config-get returns expected values.""" |
194 | 284 | state = yield self.build_default_relationships() | 319 | yield self.build_default_relationships() |
195 | 285 | 320 | ||
196 | 286 | hook_log = self.capture_logging("hook") | 321 | hook_log = self.capture_logging("hook") |
197 | 287 | 322 | ||
198 | @@ -294,13 +329,13 @@ | |||
199 | 294 | exe = self.ua.get_invoker("db", "add", "mysql/0", self.mysql_relation, | 329 | exe = self.ua.get_invoker("db", "add", "mysql/0", self.mysql_relation, |
200 | 295 | client_id="client_id") | 330 | client_id="client_id") |
201 | 296 | 331 | ||
202 | 297 | mysql = state["services"]["mysql"] | ||
203 | 298 | context = yield self.ua.get_context("client_id") | 332 | context = yield self.ua.get_context("client_id") |
204 | 299 | config = yield context.get_config() | 333 | config = yield context.get_config() |
205 | 300 | config.update(expected) | 334 | config.update(expected) |
206 | 301 | yield config.write() | 335 | yield config.write() |
207 | 302 | 336 | ||
208 | 303 | # invoke relation-get and verify the result | 337 | # invoke relation-get and verify the result |
209 | 338 | |||
210 | 304 | result = yield exe(self.create_hook("config-get", "--format=json")) | 339 | result = yield exe(self.create_hook("config-get", "--format=json")) |
211 | 305 | self.assertEqual(result, 0) | 340 | self.assertEqual(result, 0) |
212 | 306 | 341 | ||
213 | @@ -318,7 +353,12 @@ | |||
214 | 318 | yield self._default_relations() | 353 | yield self._default_relations() |
215 | 319 | self.update_invoker_env("mysql/0", "wordpress/0") | 354 | self.update_invoker_env("mysql/0", "wordpress/0") |
216 | 320 | self.socket_path = self.makeFile() | 355 | self.socket_path = self.makeFile() |
218 | 321 | self.ua = MockUnitAgent(self.client, self.socket_path) | 356 | unit_dir = self.makeDir() |
219 | 357 | self.makeDir(path=os.path.join(unit_dir, "formula")) | ||
220 | 358 | self.ua = MockUnitAgent( | ||
221 | 359 | self.client, | ||
222 | 360 | self.socket_path, | ||
223 | 361 | unit_dir) | ||
224 | 322 | self.log = self.capture_logging( | 362 | self.log = self.capture_logging( |
225 | 323 | formatter=logging.Formatter( | 363 | formatter=logging.Formatter( |
226 | 324 | "%(name)s:%(levelname)s:: %(message)s"), | 364 | "%(name)s:%(levelname)s:: %(message)s"), |
227 | 325 | 365 | ||
228 | === modified file 'ensemble/unit/lifecycle.py' | |||
229 | --- ensemble/unit/lifecycle.py 2011-05-25 20:52:05 +0000 | |||
230 | +++ ensemble/unit/lifecycle.py 2011-07-06 18:05:30 +0000 | |||
231 | @@ -309,7 +309,8 @@ | |||
232 | 309 | hook_path = os.path.join(unit_path, "formula", "hooks", hook_name) | 309 | hook_path = os.path.join(unit_path, "formula", "hooks", hook_name) |
233 | 310 | socket_path = os.path.join(unit_path, HOOK_SOCKET_FILE) | 310 | socket_path = os.path.join(unit_path, HOOK_SOCKET_FILE) |
234 | 311 | 311 | ||
236 | 312 | invoker = Invoker(None, None, "constant", socket_path, hook_log) | 312 | invoker = Invoker( |
237 | 313 | None, None, "constant", socket_path, self._unit_path, hook_log) | ||
238 | 313 | 314 | ||
239 | 314 | if now: | 315 | if now: |
240 | 315 | yield self._executor.run_priority_hook(invoker, hook_path) | 316 | yield self._executor.run_priority_hook(invoker, hook_path) |
241 | @@ -387,8 +388,9 @@ | |||
242 | 387 | hook_names = ["%s-relation-changed" % self._relation_name] | 388 | hook_names = ["%s-relation-changed" % self._relation_name] |
243 | 388 | else: | 389 | else: |
244 | 389 | hook_names = [hook_name] | 390 | hook_names = [hook_name] |
247 | 390 | invoker = RelationInvoker( | 391 | |
248 | 391 | context, change, "constant", socket_path, hook_log) | 392 | invoker = RelationInvoker(context, change, "constant", socket_path, |
249 | 393 | self._unit_path, hook_log) | ||
250 | 392 | 394 | ||
251 | 393 | for hook_name in hook_names: | 395 | for hook_name in hook_names: |
252 | 394 | hook_path = os.path.join( | 396 | hook_path = os.path.join( |
253 | 395 | 397 | ||
254 | === modified file 'ensemble/unit/tests/test_lifecycle.py' | |||
255 | --- ensemble/unit/tests/test_lifecycle.py 2011-05-26 11:20:43 +0000 | |||
256 | +++ ensemble/unit/tests/test_lifecycle.py 2011-07-06 18:05:30 +0000 | |||
257 | @@ -750,10 +750,13 @@ | |||
258 | 750 | PATH=os.environ["PATH"], | 750 | PATH=os.environ["PATH"], |
259 | 751 | ENSEMBLE_UNIT_NAME="service-unit/0") | 751 | ENSEMBLE_UNIT_NAME="service-unit/0") |
260 | 752 | change = RelationChange("clients", "joined", "s/2") | 752 | change = RelationChange("clients", "joined", "s/2") |
262 | 753 | invoker = RelationInvoker(None, change, "", "", None) | 753 | unit_hook_path = self.makeDir() |
263 | 754 | invoker = RelationInvoker(None, change, "", "", unit_hook_path, None) | ||
264 | 754 | environ = invoker.get_environment() | 755 | environ = invoker.get_environment() |
265 | 755 | self.assertEqual(environ["ENSEMBLE_RELATION"], "clients") | 756 | self.assertEqual(environ["ENSEMBLE_RELATION"], "clients") |
266 | 756 | self.assertEqual(environ["ENSEMBLE_REMOTE_UNIT"], "s/2") | 757 | self.assertEqual(environ["ENSEMBLE_REMOTE_UNIT"], "s/2") |
267 | 758 | self.assertEqual(environ["FORMULA_DIR"], | ||
268 | 759 | os.path.join(unit_hook_path, "formula")) | ||
269 | 757 | 760 | ||
270 | 758 | 761 | ||
271 | 759 | class UnitRelationLifecycleTest(LifecycleTestBase): | 762 | class UnitRelationLifecycleTest(LifecycleTestBase): |
[1]
We talked about having the formula path as the cwd, and I've seen at least one person mentioning the expectation that the cwd is actually formula/hooks. I'd probably be worth to do a quick individual questioning about what each formula author we have expects cwd to be, and use that instead of the unit dir.