etcutils 0.1.5-x86-linux
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +21 -0
- data/LICENSE +20 -0
- data/README.md +322 -0
- data/Rakefile +15 -0
- data/etcutils.gemspec +24 -0
- data/ext/etcutils/etcutils.c +1182 -0
- data/ext/etcutils/etcutils.h +98 -0
- data/ext/etcutils/extconf.rb +45 -0
- data/tests/etcutils_test_helper.rb +4 -0
- data/tests/test_etc_utils.rb +71 -0
- metadata +86 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
data.tar.gz: 78693dd7e391e09bf3217755eec9d8eb5be4c7c3
|
4
|
+
metadata.gz: c457e9afacd35cce648123f0e14d9d7440537621
|
5
|
+
SHA512:
|
6
|
+
data.tar.gz: 70678a1790e113347c0f781aa2e48e750a237bb2b342a8587a3286e02a5f66ddb02e3b74abf4ef412d86fa190b369b12878c6b1c550cffef6cd902bcfb0a727a
|
7
|
+
metadata.gz: a4b448b257d94dcce0efc79e1582162d888db68b9e4fde01a9071510555c8dc1514bcd14979ec3654399ea73815ae12baf062eac27e4cf2014f0563e52f939a2
|
data/.gitignore
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2013 David Campbell
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,322 @@
|
|
1
|
+
EtcUtils
|
2
|
+
======
|
3
|
+
|
4
|
+
Ruby C Extension for read and write access to the Linux user database.
|
5
|
+
|
6
|
+
gem install etcutils
|
7
|
+
|
8
|
+
This gem can have catastrophic effects on your system if used incorrectly. I eventually would like to get to the point that it does all the hard thinking for you when it comes to writing to file, but for now you'll need to write your own tmp file (like /etc/passwd-) before moving it into place. This has worked for years for ```useradd``` and ```adduser``` and you should try keep that same mentality.
|
9
|
+
|
10
|
+
## Deprecation Warning
|
11
|
+
|
12
|
+
NOTE: In i386, #to_s was removed in 0.1.5.
|
13
|
+
|
14
|
+
In the transitional release 0.1.5, the Struct method #to_s should no longer be used to print UserDB style strings. I'm not printing a warning when it is used, however, since #inspect calls #to_s and warnings would get annoying quickly.
|
15
|
+
|
16
|
+
The #to_s method will be removed from EtcUtils in release 1.0.0 (see feature/classes), which is scheduled to be released in late May/early June.
|
17
|
+
|
18
|
+
Moving forward, please use #to_entry in-place of #to_s. See [parse](#parse) for more.
|
19
|
+
|
20
|
+
I apologize for the inconvience.
|
21
|
+
|
22
|
+
|
23
|
+
## Know Issues
|
24
|
+
|
25
|
+
Verified on Ubuntu 12.04, nsswitch.conf is misconfigured due to a known [bug](http://bugs.debian.org/cgi-bin/bugreport.cgi?bug=699089). You will be unable to manipulate /etc/gshadow until this line is added to /etc/nsswitch.conf.
|
26
|
+
|
27
|
+
gshadow: files
|
28
|
+
|
29
|
+
|
30
|
+
## Structs
|
31
|
+
|
32
|
+
##### EtcUtils::Passwd
|
33
|
+
|
34
|
+
```ruby
|
35
|
+
EtcUtils.find_pwd(1 || 'daemon')
|
36
|
+
EtcUtils::Passwd.find('daemon' || 1)
|
37
|
+
=> # <struct EtcUtils::Passwd name="daemon", passwd="x", uid=1, gid=1, gecos="daemon", dir="/usr/sbin", shell="/bin/sh">
|
38
|
+
```
|
39
|
+
|
40
|
+
* `:name`: This is the user’s login name. It should not contain capital letters.
|
41
|
+
|
42
|
+
* `:passwd`: This is either the encrypted user password, an asterisk (*), or the letter aqxaq. (See `pwconv(8)` for an explanation of aqxaq.)
|
43
|
+
|
44
|
+
* `:uid`: The privileged root login account (superuser) has the user ID 0.
|
45
|
+
|
46
|
+
* `:gid`: This is the numeric primary group ID for this user. (Additional groups for the user are defined in the system group file; see `group(5)`).
|
47
|
+
|
48
|
+
* `:gecos`: This field (sometimes called the “comment field”) is optional and used only for informational purposes. Usually, it contains the full username. Some programs (for example, `finger(1)`) display information from this field. GECOS stands for “General Electric Comprehensive Operating System”, which was renamed to GCOS when GE’s large systems division was sold to Honeywell. Dennis Ritchie has reported: “Sometimes we sent printer output or batch jobs to the GCOS machine. The gcos field in the password file was a place to stash the information for the $IDENTcard. Not elegant.”
|
49
|
+
|
50
|
+
* `:dir`: This is the user’s home directory: the initial directory where the user is placed after logging in. The value in this field is used to set the HOME environment variable.
|
51
|
+
|
52
|
+
* `:shell`: This is the program to run at login (if empty, use /bin/sh). If set to a nonexistent executable, the user will be unable to login through `login(1)`. The value in this field is used to set the SHELL environment variable.
|
53
|
+
|
54
|
+
|
55
|
+
##### EtcUtils::Group
|
56
|
+
|
57
|
+
```ruby
|
58
|
+
EtcUtils.find_grp(1 || 'daemon')
|
59
|
+
EtcUtils::Group.find('daemon' || 1)
|
60
|
+
=> #<struct EtcUtils::Group name="daemon", passwd="x", gid=1, members=[]>
|
61
|
+
```
|
62
|
+
|
63
|
+
* `:name`: The name of the group.
|
64
|
+
|
65
|
+
* `:passwd`: The (encrypted) group password. If this field is empty, no password is needed.
|
66
|
+
|
67
|
+
* `:gid`: The numeric group ID.
|
68
|
+
|
69
|
+
* `:members`: A list of the usernames that are members of this group, separated by commas.
|
70
|
+
|
71
|
+
##### EtcUtils::Shadow
|
72
|
+
|
73
|
+
The user must be able to read `SHADOW` or nil is returned.
|
74
|
+
|
75
|
+
```ruby
|
76
|
+
SHADOW
|
77
|
+
=> "/etc/shadow"
|
78
|
+
EtcUtils.find_spwd(1 || 'daemon')
|
79
|
+
EtcUtils::Shadow.find('daemon' || 1)
|
80
|
+
=> #<struct EtcUtils::Shadow name="daemon", passwd="*", last_change=15729, min_change=0, max_change=99999, warn=7, inactive=-1, expire=-1, flag=-1>
|
81
|
+
```
|
82
|
+
|
83
|
+
* `:name`: This is the user’s login name. It should not contain capital letters.
|
84
|
+
|
85
|
+
* `:passwd`: Refer to crypt(3) for details on how this string is interpreted. If the password field contains some string that is not a valid result of crypt(3), for instance ! or *, the user will not be able to use a unix password to log in (but the user may log in the system by other means). This field may be empty, in which case no passwords are required to authenticate as the specified login name. However, some applications which read the /etc/shadow file may decide not to permit any access at all if the password field is empty. A password field which starts with a exclamation mark means that the password is locked. The remaining characters on the line represent the password field before the password was locked.
|
86
|
+
|
87
|
+
* `:last_change`: The date of the last password change, expressed as the number of days since Jan 1, 1970. The value 0 has a special meaning, which is that the user should change her pasword the next time she will log in the system. An empty field means that password aging features are disabled.
|
88
|
+
|
89
|
+
* `:min_change`: The minimum password age is the number of days the user will have to wait before she will be allowed to change her password again. An empty field and value 0 mean that there are no minimum password age.
|
90
|
+
|
91
|
+
* `:max_change`: The maximum password age is the number of days after which the user will have to change her password. After this number of days is elapsed, the password may still be valid. The user should be asked to change her password the next time she will log in. An empty field means that there are no maximum password age, no password warning period, and no password inactivity period (see below). If the maximum password age is lower than the minimum password age, the user cannot change her password.
|
92
|
+
|
93
|
+
* `:warn`: The number of days before a password is going to expire (see the maximum password age above) during which the user should be warned. An empty field and value 0 mean that there are no password warning period.
|
94
|
+
|
95
|
+
* `:inactive`: The number of days after a password has expired (see the maximum password age above) during which the password should still be accepted (and the user should update her password during the next login). After expiration of the password and this expiration period is elapsed, no login is possible using the current user’s password. The user should contact her administrator. An empty field means that there are no enforcement of an inactivity period.
|
96
|
+
|
97
|
+
* `:expire`: The date of expiration of the account, expressed as the number of days since Jan 1, 1970. Note that an account expiration differs from a password expiration. In case of an acount expiration, the user shall not be allowed to login. In case of a password expiration, the user is not allowed to login using her password. An empty field means that the account will never expire. The value 0 should not be used as it is interpreted as either an account with no expiration, or as an expiration on Jan 1, 1970.
|
98
|
+
|
99
|
+
* `:flag`: This field is reserved for future use.
|
100
|
+
|
101
|
+
|
102
|
+
##### EtcUtils::GShadow
|
103
|
+
|
104
|
+
If you're having trouble with GShadow, even as root, please see [known issues](#known-issues). Your nsswitch.conf file may contain a known bug.
|
105
|
+
|
106
|
+
The user must be able to read `GSHADOW` or nil is returned.
|
107
|
+
|
108
|
+
```ruby
|
109
|
+
GSHADOW
|
110
|
+
=> "/etc/gshadow"
|
111
|
+
EtcUtils.find_sgrp('daemon' || 1)
|
112
|
+
EtcUtils::GShadow.find('daemon' || 1)
|
113
|
+
EtcUtils::Gshadow.find('daemon' || 1)
|
114
|
+
=> #<struct EtcUtils::GShadow name="daemon", passwd="*", admins=[], members=[]>
|
115
|
+
```
|
116
|
+
|
117
|
+
* `:name`: It must be a valid group name, which exist on the system.
|
118
|
+
|
119
|
+
* `:passwd`: Refer to `crypt(3)` for details on how this string is interpreted. If the password field contains some string that is not a valid result of `crypt(3)`, for instance ! or *, users will not be able to use a unix password to access the group (but group members do not need the password). The password is used when an user who is not a member of the group wants to gain the permissions of this group (see `newgrp(1)`). This field may be empty, in which case only the group members can gain the group permissions. A password field which starts with a exclamation mark means that the password is locked. The remaining characters on the line represent the password field before the password was locked. This password supersedes any password specified in _/etc/group_.
|
120
|
+
|
121
|
+
* `:admins`: It must be a comma-separated list of user names. Administrators can change the password or the members of the group. Administrators also have the same permissions as the members (see below).
|
122
|
+
|
123
|
+
* `:members`: It must be a comma-separated list of user names. Members can access the group without being prompted for a password. You should use the same list of users as in _/etc/group_.
|
124
|
+
|
125
|
+
|
126
|
+
# Usage
|
127
|
+
|
128
|
+
## File locks
|
129
|
+
|
130
|
+
Passing a block to `EtcUtils.lock` is the preferred method of locking files, as unlock is always called regardless of exceptions
|
131
|
+
|
132
|
+
```ruby
|
133
|
+
> begin
|
134
|
+
> lock {
|
135
|
+
> puts "INSIDE BLOCK: #{locked?}"
|
136
|
+
> raise "foobar"
|
137
|
+
> }
|
138
|
+
> rescue
|
139
|
+
> puts "RESCUED: #{locked?}"
|
140
|
+
> end
|
141
|
+
=> INSIDE BLOCK: true
|
142
|
+
=> RESCUED: false
|
143
|
+
```
|
144
|
+
|
145
|
+
`:lckpwdf` `:lock`: Locking files will return true if locked. Calling lock on a locked file will return true. A return of false indicates lock failure.
|
146
|
+
|
147
|
+
`:ulckpwdf` `:unlock`: Unlocking will return true upon success. Subsequent calls against an unlocked file will return false.
|
148
|
+
|
149
|
+
To keep confusion at bay, calling `:locked?` will always return the true state of the lock.
|
150
|
+
|
151
|
+
```ruby
|
152
|
+
EtcUtils.lckpwdf
|
153
|
+
=> true
|
154
|
+
EtcUtils.lock
|
155
|
+
=> true
|
156
|
+
EtcUtils.lock
|
157
|
+
=> true
|
158
|
+
EtcUtils.locked?
|
159
|
+
=> true
|
160
|
+
EtcUtils.ulckpwdf
|
161
|
+
=> true
|
162
|
+
EtcUtils.ulckpwdf
|
163
|
+
=> false
|
164
|
+
EtcUtils.unlock
|
165
|
+
=> false
|
166
|
+
EtcUtils.locked?
|
167
|
+
=> false
|
168
|
+
```
|
169
|
+
|
170
|
+
---
|
171
|
+
|
172
|
+
The below should apply to any of the above structs. If you find a bug, <b>please</b> open an issue.
|
173
|
+
|
174
|
+
XX can be replaced with
|
175
|
+
|
176
|
+
* pw # PASSWD
|
177
|
+
* gr # GROUP
|
178
|
+
* sp # SHADOW
|
179
|
+
* sg # GSHADOW
|
180
|
+
|
181
|
+
## setXXent and getXXent
|
182
|
+
|
183
|
+
The `EtcUtils.setpwent` and `EtcUtils::Passwd.set` functions rewind to the beginning of the password database.
|
184
|
+
|
185
|
+
The `EtcUtils.endpwent` and `EtcUtils::Passwd.end` functions are used to close the password database after all processing has been performed.
|
186
|
+
|
187
|
+
Helper functions (literal 'XX') `EtcUtils.setXXent` and `EtcUtils.endXXent` rewind or close all database files.
|
188
|
+
|
189
|
+
|
190
|
+
## Retrieving Entries
|
191
|
+
|
192
|
+
`#set` is not required on the initial attempt at retrieving entries.
|
193
|
+
|
194
|
+
### getXXent
|
195
|
+
|
196
|
+
Calling `EtcUtils.getgrent` and `EtcUtils::Group.get` retrieves the first entry from the group database.
|
197
|
+
|
198
|
+
```ruby
|
199
|
+
EtcUtils.getgrent
|
200
|
+
=> #<struct EtcUtils::Group name="root", passwd="x", gid=0, members=[]>
|
201
|
+
...
|
202
|
+
EtcUtils::Group.get
|
203
|
+
=> #<struct EtcUtils::Group name="adm", passwd="x", gid=4, members=["ubuntu", "foobar"]>
|
204
|
+
EtcUtils.setgrent
|
205
|
+
=> nil
|
206
|
+
EtcUtils::Group.get
|
207
|
+
=> #<struct EtcUtils::Group name="root", passwd="x", gid=0, members=[]>
|
208
|
+
```
|
209
|
+
|
210
|
+
### find
|
211
|
+
|
212
|
+
`EtcUtils::GShadow.find` can be called with either a string or an integer. In the case of shadow files, the corresponding etc file will be queried by uid/gid and returned.
|
213
|
+
|
214
|
+
```ruby
|
215
|
+
EtcUtils::GShadow.find 'adm'
|
216
|
+
=> #<struct EtcUtils::Group name="adm", passwd="x", gid=4, members=["ubuntu", "foobar"]>
|
217
|
+
EtcUtils::GShadow.find 4
|
218
|
+
=> #<struct EtcUtils::Group name="adm", passwd="x", gid=4, members=["ubuntu", "foobar"]>
|
219
|
+
```
|
220
|
+
|
221
|
+
## New Entries
|
222
|
+
|
223
|
+
### parse
|
224
|
+
|
225
|
+
If an entry already exists in the corresponding file, that object (un-altered) is returned.
|
226
|
+
|
227
|
+
```ruby
|
228
|
+
p = EtcUtils::Passwd.find 1
|
229
|
+
=> #<struct EtcUtils::Passwd name="daemon", passwd="x", uid=1, gid=1, gecos="daemon", dir="/usr/sbin", shell="/bin/sh">
|
230
|
+
p.uid = 9999
|
231
|
+
=> 9999
|
232
|
+
p.to_entry
|
233
|
+
=> "daemon:x:9999:1:daemon:/usr/sbin:/bin/sh"
|
234
|
+
EtcUtils::Passwd.parse(p.to_entry)
|
235
|
+
=> #<struct EtcUtils::Passwd name="daemon", passwd="x", uid=1, gid=1, gecos="daemon", dir="/usr/sbin", shell="/bin/sh">
|
236
|
+
```
|
237
|
+
|
238
|
+
If no entry is found, the new object is returned.
|
239
|
+
|
240
|
+
If uid/gid fields are left blank, the next available id is returned.
|
241
|
+
|
242
|
+
```ruby
|
243
|
+
EtcUtils::Passwd.parse("foobar:x:::Foobar User:/home/foobar:/bin/shell")
|
244
|
+
=> #<struct EtcUtils::Passwd name="foobar", passwd="x", uid=11, gid=11, gecos="Foobar User", dir="/home/foobar", shell="/bin/shell">
|
245
|
+
```
|
246
|
+
### new
|
247
|
+
|
248
|
+
When called without args, an empty struct is returned. When called with args, those args are used to populate the object.
|
249
|
+
|
250
|
+
```ruby
|
251
|
+
EtcUtils::GShadow.new("foobar", '!', nil, ['sudo','adm'])
|
252
|
+
=> #<struct EtcUtils::GShadow name="foobar", passwd="!", admins=nil, members=["sudo", "adm"]>
|
253
|
+
EtcUtils::GShadow.new
|
254
|
+
=> #<struct EtcUtils::GShadow name=nil, passwd=nil, admins=nil, members=nil>
|
255
|
+
```
|
256
|
+
## Writing Entries
|
257
|
+
|
258
|
+
**Please be careful when you're writing to user database files.**
|
259
|
+
|
260
|
+
Before writing to any file, you should first create a backup. Conventional nomenclature is to append a dash to the end of the string. You'll also need you preserve file permissions. See `File.stat`.
|
261
|
+
|
262
|
+
```ruby
|
263
|
+
GSHADOW + '-'
|
264
|
+
=> "/etc/gshadow-"
|
265
|
+
|
266
|
+
stat = File.stat(GSHADOW).dup
|
267
|
+
File.open(GSHADOW + '-', 'w+', 0600) { |bf| bf.puts IO.readlines(GSHADOW) }
|
268
|
+
|
269
|
+
if stat.size != File.stat(GSHADOW + '-').size
|
270
|
+
raise_retry "#{GSHADOW} backup error"
|
271
|
+
end
|
272
|
+
```
|
273
|
+
|
274
|
+
To update etc files, you should write all entries, including your updates, first to a temp file.
|
275
|
+
|
276
|
+
```ruby
|
277
|
+
tmp = "/etc/_#{SHADOW.split('/').last}"
|
278
|
+
fh = File.open(SHADOW, 'r')
|
279
|
+
|
280
|
+
EtcUtils.lock {
|
281
|
+
File.open(tmp, File::RDWR|File::CREAT, 0600) { |tmp_fh|
|
282
|
+
while ( ent = EtcUtils::Shadow.get )
|
283
|
+
ent.fputs(tmp_fh)
|
284
|
+
end
|
285
|
+
}
|
286
|
+
}
|
287
|
+
fh.close
|
288
|
+
```
|
289
|
+
|
290
|
+
## Next UID/GID
|
291
|
+
|
292
|
+
`:next_uid` will store and increment the next available uid. Each time next_uid is called, that counter is incremented to the next available.
|
293
|
+
|
294
|
+
`:next_gid` will store and increment the next available gid. Each time next_gid is called, that counter is incremented to the next available.
|
295
|
+
|
296
|
+
```ruby
|
297
|
+
EtcUtils.next_uid = 12
|
298
|
+
=> 12
|
299
|
+
EtcUtils.next_uid
|
300
|
+
=> 12
|
301
|
+
EtcUtils.next_uid
|
302
|
+
=> 14
|
303
|
+
```
|
304
|
+
|
305
|
+
`:parse` assigns `:next_uid` to `:next_gid` then confirmed as available. This attempts uid/gid in sync for new users.
|
306
|
+
|
307
|
+
|
308
|
+
```ruby
|
309
|
+
EtcUtils.next_uid = 1000
|
310
|
+
=> 1000
|
311
|
+
EtcUtils::Passwd.parse("foobar:x:::Foobar User:/home/foobar:/bin/shell")
|
312
|
+
=> #<struct EtcUtils::Passwd name="foobar", passwd="x", uid=1016, gid=1016, gecos="Foobar User", dir="/home/foobar", shell="/bin/shell">
|
313
|
+
```
|
314
|
+
|
315
|
+
Although, when calling `:next_uid` or `:next_gid` they are not kept in sync. When creating new entries, it's recommended allow parse or new to manage uid/gids rather than assigning them yourself.
|
316
|
+
|
317
|
+
```ruby
|
318
|
+
EtcUtils.next_uid = 19
|
319
|
+
=> 19
|
320
|
+
EtcUtils.next_gid
|
321
|
+
=> 1017
|
322
|
+
```
|
data/Rakefile
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
require "rake"
|
2
|
+
require "rake/testtask"
|
3
|
+
require "bundler/gem_tasks"
|
4
|
+
require 'rake/extensiontask'
|
5
|
+
|
6
|
+
desc "Default: run unit tests."
|
7
|
+
task :default => :test
|
8
|
+
|
9
|
+
desc "Test EtcUtils"
|
10
|
+
Rake::TestTask.new(:test) do |t|
|
11
|
+
t.libs << "tests"
|
12
|
+
t.test_files = FileList['tests/**/test_*.rb']
|
13
|
+
end
|
14
|
+
|
15
|
+
Rake::ExtensionTask.new('etcutils')
|
data/etcutils.gemspec
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
|
3
|
+
Gem::Specification.new do |spec|
|
4
|
+
spec.platform = Gem::Platform::CURRENT
|
5
|
+
spec.name = "etcutils"
|
6
|
+
spec.version = "0.1.5"
|
7
|
+
spec.author = "David Campbell"
|
8
|
+
spec.email = "david@mrcampbell.org"
|
9
|
+
spec.description = "Ruby Extension that allows for reads and writes to the /etc user db."
|
10
|
+
spec.summary = %q{This gem is specific to *nix environments. It's a rewrite of libshadow-ruby allowing for reads and writes to passwd,shadow,group, and gshadow /etc files. There are probably still bugs, so use at your own risk.}
|
11
|
+
spec.homepage = "https://github.com/dacamp/etcutils"
|
12
|
+
spec.has_rdoc = false
|
13
|
+
|
14
|
+
spec.license = 'MIT'
|
15
|
+
spec.require_path = '.'
|
16
|
+
spec.files = `git ls-files`.split($/)
|
17
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
18
|
+
|
19
|
+
spec.extensions = ['ext/etcutils/extconf.rb']
|
20
|
+
|
21
|
+
spec.add_development_dependency "bundler", "~> 1.3"
|
22
|
+
spec.add_development_dependency "rake"
|
23
|
+
spec.add_development_dependency "rake-compiler"
|
24
|
+
end
|