all: make emulator command a string instead of a []string
This matches the flash-command and is generally a bit easier to work with. This commit also prepares for allowing multiple formats to be used in the emulator command, which is necessary for the esp32.
Этот коммит содержится в:
		
							родитель
							
								
									4fe3a379a5
								
							
						
					
					
						коммит
						bd56636d58
					
				
					 15 изменённых файлов: 82 добавлений и 74 удалений
				
			
		|  | @ -10,6 +10,7 @@ import ( | |||
| 	"regexp" | ||||
| 	"strings" | ||||
| 
 | ||||
| 	"github.com/google/shlex" | ||||
| 	"github.com/tinygo-org/tinygo/goenv" | ||||
| ) | ||||
| 
 | ||||
|  | @ -494,13 +495,38 @@ func (c *Config) WasmAbi() string { | |||
| 	return c.Target.WasmAbi | ||||
| } | ||||
| 
 | ||||
| // Emulator returns the emulator target config | ||||
| func (c *Config) Emulator() []string { | ||||
| 	var emulator []string | ||||
| 	for _, s := range c.Target.Emulator { | ||||
| 		emulator = append(emulator, strings.ReplaceAll(s, "{root}", goenv.Get("TINYGOROOT"))) | ||||
| // EmulatorName is a shorthand to get the command for this emulator, something | ||||
| // like qemu-system-arm or simavr. | ||||
| func (c *Config) EmulatorName() string { | ||||
| 	parts := strings.SplitN(c.Target.Emulator, " ", 2) | ||||
| 	if len(parts) > 1 { | ||||
| 		return parts[0] | ||||
| 	} | ||||
| 	return emulator | ||||
| 	return "" | ||||
| } | ||||
| 
 | ||||
| // EmulatorFormat returns the binary format for the emulator and the associated | ||||
| // file extension. An empty string means to pass directly whatever the linker | ||||
| // produces directly without conversion. | ||||
| func (c *Config) EmulatorFormat() (format, fileExt string) { | ||||
| 	return "", "" | ||||
| } | ||||
| 
 | ||||
| // Emulator returns a ready-to-run command to run the given binary in an | ||||
| // emulator. Give it the format (returned by EmulatorFormat()) and the path to | ||||
| // the compiled binary. | ||||
| func (c *Config) Emulator(format, binary string) ([]string, error) { | ||||
| 	parts, err := shlex.Split(c.Target.Emulator) | ||||
| 	if err != nil { | ||||
| 		return nil, fmt.Errorf("could not parse emulator command: %w", err) | ||||
| 	} | ||||
| 	var emulator []string | ||||
| 	for _, s := range parts { | ||||
| 		s = strings.ReplaceAll(s, "{root}", goenv.Get("TINYGOROOT")) | ||||
| 		s = strings.ReplaceAll(s, "{"+format+"}", binary) | ||||
| 		emulator = append(emulator, s) | ||||
| 	} | ||||
| 	return emulator, nil | ||||
| } | ||||
| 
 | ||||
| type TestConfig struct { | ||||
|  |  | |||
|  | @ -44,8 +44,8 @@ type TargetSpec struct { | |||
| 	LDFlags          []string `json:"ldflags"` | ||||
| 	LinkerScript     string   `json:"linkerscript"` | ||||
| 	ExtraFiles       []string `json:"extra-files"` | ||||
| 	RP2040BootPatch  *bool    `json:"rp2040-boot-patch"`        // Patch RP2040 2nd stage bootloader checksum | ||||
| 	Emulator         []string `json:"emulator" override:"copy"` // inherited Emulator must not be append | ||||
| 	RP2040BootPatch  *bool    `json:"rp2040-boot-patch"` // Patch RP2040 2nd stage bootloader checksum | ||||
| 	Emulator         string   `json:"emulator"` | ||||
| 	FlashCommand     string   `json:"flash-command"` | ||||
| 	GDB              []string `json:"gdb"` | ||||
| 	PortReset        string   `json:"flash-1200-bps-reset"` | ||||
|  | @ -90,19 +90,8 @@ func (spec *TargetSpec) overrideProperties(child *TargetSpec) { | |||
| 			if !src.IsNil() { | ||||
| 				dst.Set(src) | ||||
| 			} | ||||
| 		case reflect.Slice: // for slices... | ||||
| 			if src.Len() > 0 { // ... if not empty ... | ||||
| 				switch tag := field.Tag.Get("override"); tag { | ||||
| 				case "copy": | ||||
| 					// copy the field of child to spec | ||||
| 					dst.Set(src) | ||||
| 				case "append", "": | ||||
| 					// or append the field of child to spec | ||||
| 					dst.Set(reflect.AppendSlice(dst, src)) | ||||
| 				default: | ||||
| 					panic("override mode must be 'copy' or 'append' (default). I don't know how to '" + tag + "'.") | ||||
| 				} | ||||
| 			} | ||||
| 		case reflect.Slice: // for slices, append the field | ||||
| 			dst.Set(reflect.AppendSlice(dst, src)) | ||||
| 		default: | ||||
| 			panic("unknown field type : " + kind.String()) | ||||
| 		} | ||||
|  | @ -335,20 +324,20 @@ func defaultTarget(goos, goarch, triple string) (*TargetSpec, error) { | |||
| 			case "386": | ||||
| 				// amd64 can _usually_ run 32-bit programs, so skip the emulator in that case. | ||||
| 				if runtime.GOARCH != "amd64" { | ||||
| 					spec.Emulator = []string{"qemu-i386"} | ||||
| 					spec.Emulator = "qemu-i386 {}" | ||||
| 				} | ||||
| 			case "amd64": | ||||
| 				spec.Emulator = []string{"qemu-x86_64"} | ||||
| 				spec.Emulator = "qemu-x86_64 {}" | ||||
| 			case "arm": | ||||
| 				spec.Emulator = []string{"qemu-arm"} | ||||
| 				spec.Emulator = "qemu-arm {}" | ||||
| 			case "arm64": | ||||
| 				spec.Emulator = []string{"qemu-aarch64"} | ||||
| 				spec.Emulator = "qemu-aarch64 {}" | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 	if goos != runtime.GOOS { | ||||
| 		if goos == "windows" { | ||||
| 			spec.Emulator = []string{"wine"} | ||||
| 			spec.Emulator = "wine {}" | ||||
| 		} | ||||
| 	} | ||||
| 	return &spec, nil | ||||
|  |  | |||
|  | @ -29,7 +29,6 @@ func TestOverrideProperties(t *testing.T) { | |||
| 		CPU:              "baseCpu", | ||||
| 		CFlags:           []string{"-base-foo", "-base-bar"}, | ||||
| 		BuildTags:        []string{"bt1", "bt2"}, | ||||
| 		Emulator:         []string{"be1", "be2"}, | ||||
| 		DefaultStackSize: 42, | ||||
| 		AutoStackSize:    &baseAutoStackSize, | ||||
| 	} | ||||
|  | @ -38,7 +37,6 @@ func TestOverrideProperties(t *testing.T) { | |||
| 		GOOS:             "", | ||||
| 		CPU:              "chlidCpu", | ||||
| 		CFlags:           []string{"-child-foo", "-child-bar"}, | ||||
| 		Emulator:         []string{"ce1", "ce2"}, | ||||
| 		AutoStackSize:    &childAutoStackSize, | ||||
| 		DefaultStackSize: 64, | ||||
| 	} | ||||
|  | @ -57,9 +55,6 @@ func TestOverrideProperties(t *testing.T) { | |||
| 	if !reflect.DeepEqual(base.BuildTags, []string{"bt1", "bt2"}) { | ||||
| 		t.Errorf("Overriding failed : got %v", base.BuildTags) | ||||
| 	} | ||||
| 	if !reflect.DeepEqual(base.Emulator, []string{"ce1", "ce2"}) { | ||||
| 		t.Errorf("Overriding failed : got %v", base.Emulator) | ||||
| 	} | ||||
| 	if *base.AutoStackSize != false { | ||||
| 		t.Errorf("Overriding failed : got %v", base.AutoStackSize) | ||||
| 	} | ||||
|  |  | |||
							
								
								
									
										53
									
								
								main.go
									
										
									
									
									
								
							
							
						
						
									
										53
									
								
								main.go
									
										
									
									
									
								
							|  | @ -239,8 +239,7 @@ func Test(pkgName string, stdout, stderr io.Writer, options *compileopts.Options | |||
| 		cmd.Dir = result.MainDir | ||||
| 
 | ||||
| 		// Wasmtime needs a few extra flags to work. | ||||
| 		emulator := config.Emulator() | ||||
| 		if len(emulator) != 0 && emulator[0] == "wasmtime" { | ||||
| 		if config.EmulatorName() == "wasmtime" { | ||||
| 			// Add directories to the module root, but skip the current working | ||||
| 			// directory which is already added by buildAndRun. | ||||
| 			dirs := dirsToModuleRoot(result.MainDir, result.ModuleRoot) | ||||
|  | @ -484,18 +483,19 @@ func Debug(debugger, pkgName string, ocdOutput bool, options *compileopts.Option | |||
| 		return err | ||||
| 	} | ||||
| 
 | ||||
| 	return builder.Build(pkgName, "", config, func(result builder.BuildResult) error { | ||||
| 	format, fileExt := config.EmulatorFormat() | ||||
| 	return builder.Build(pkgName, fileExt, config, func(result builder.BuildResult) error { | ||||
| 		// Find a good way to run GDB. | ||||
| 		gdbInterface, openocdInterface := config.Programmer() | ||||
| 		switch gdbInterface { | ||||
| 		case "msd", "command", "": | ||||
| 			emulator := config.Emulator() | ||||
| 			if len(emulator) != 0 { | ||||
| 				if emulator[0] == "mgba" { | ||||
| 			emulator := config.EmulatorName() | ||||
| 			if emulator != "" { | ||||
| 				if emulator == "mgba" { | ||||
| 					gdbInterface = "mgba" | ||||
| 				} else if emulator[0] == "simavr" { | ||||
| 				} else if emulator == "simavr" { | ||||
| 					gdbInterface = "simavr" | ||||
| 				} else if strings.HasPrefix(emulator[0], "qemu-system-") { | ||||
| 				} else if strings.HasPrefix(emulator, "qemu-system-") { | ||||
| 					gdbInterface = "qemu" | ||||
| 				} else { | ||||
| 					// Assume QEMU as an emulator. | ||||
|  | @ -514,6 +514,10 @@ func Debug(debugger, pkgName string, ocdOutput bool, options *compileopts.Option | |||
| 		port := "" | ||||
| 		var gdbCommands []string | ||||
| 		var daemon *exec.Cmd | ||||
| 		emulator, err := config.Emulator(format, result.Binary) | ||||
| 		if err != nil { | ||||
| 			return err | ||||
| 		} | ||||
| 		switch gdbInterface { | ||||
| 		case "native": | ||||
| 			// Run GDB directly. | ||||
|  | @ -563,33 +567,29 @@ func Debug(debugger, pkgName string, ocdOutput bool, options *compileopts.Option | |||
| 			} | ||||
| 		case "qemu": | ||||
| 			port = ":1234" | ||||
| 			emulator := config.Emulator() | ||||
| 			// Run in an emulator. | ||||
| 			args := append(emulator[1:], result.Binary, "-s", "-S") | ||||
| 			args := append(emulator[1:], "-s", "-S") | ||||
| 			daemon = executeCommand(config.Options, emulator[0], args...) | ||||
| 			daemon.Stdout = os.Stdout | ||||
| 			daemon.Stderr = os.Stderr | ||||
| 		case "qemu-user": | ||||
| 			port = ":1234" | ||||
| 			emulator := config.Emulator() | ||||
| 			// Run in an emulator. | ||||
| 			args := append(emulator[1:], "-g", "1234", result.Binary) | ||||
| 			args := append(emulator[1:], "-g", "1234") | ||||
| 			daemon = executeCommand(config.Options, emulator[0], args...) | ||||
| 			daemon.Stdout = os.Stdout | ||||
| 			daemon.Stderr = os.Stderr | ||||
| 		case "mgba": | ||||
| 			port = ":2345" | ||||
| 			emulator := config.Emulator() | ||||
| 			// Run in an emulator. | ||||
| 			args := append(emulator[1:], result.Binary, "-g") | ||||
| 			args := append(emulator[1:], "-g") | ||||
| 			daemon = executeCommand(config.Options, emulator[0], args...) | ||||
| 			daemon.Stdout = os.Stdout | ||||
| 			daemon.Stderr = os.Stderr | ||||
| 		case "simavr": | ||||
| 			port = ":1234" | ||||
| 			emulator := config.Emulator() | ||||
| 			// Run in an emulator. | ||||
| 			args := append(emulator[1:], "-g", result.Binary) | ||||
| 			args := append(emulator[1:], "-g") | ||||
| 			daemon = executeCommand(config.Options, emulator[0], args...) | ||||
| 			daemon.Stdout = os.Stdout | ||||
| 			daemon.Stderr = os.Stderr | ||||
|  | @ -666,7 +666,7 @@ func Debug(debugger, pkgName string, ocdOutput bool, options *compileopts.Option | |||
| 		cmd.Stdin = os.Stdin | ||||
| 		cmd.Stdout = os.Stdout | ||||
| 		cmd.Stderr = os.Stderr | ||||
| 		err := cmd.Run() | ||||
| 		err = cmd.Run() | ||||
| 		if err != nil { | ||||
| 			return &commandError{"failed to run " + cmdName + " with", result.Binary, err} | ||||
| 		} | ||||
|  | @ -694,9 +694,6 @@ func Run(pkgName string, options *compileopts.Options, cmdArgs []string) error { | |||
| // passes command line arguments and evironment variables in a way appropriate | ||||
| // for the given emulator. | ||||
| func buildAndRun(pkgName string, config *compileopts.Config, stdout io.Writer, cmdArgs, environmentVars []string, timeout time.Duration, run func(cmd *exec.Cmd, result builder.BuildResult) error) error { | ||||
| 	// make sure any special vars in the emulator definition are rewritten | ||||
| 	emulator := config.Emulator() | ||||
| 
 | ||||
| 	// Determine whether we're on a system that supports environment variables | ||||
| 	// and command line parameters (operating systems, WASI) or not (baremetal, | ||||
| 	// WebAssembly in the browser). If we're on a system without an environment, | ||||
|  | @ -728,7 +725,7 @@ func buildAndRun(pkgName string, config *compileopts.Config, stdout io.Writer, c | |||
| 				"runtime": runtimeGlobals, | ||||
| 			} | ||||
| 		} | ||||
| 	} else if len(emulator) != 0 && emulator[0] == "wasmtime" { | ||||
| 	} else if config.EmulatorName() == "wasmtime" { | ||||
| 		// Wasmtime needs some special flags to pass environment variables | ||||
| 		// and allow reading from the current directory. | ||||
| 		args = append(args, "--dir=.") | ||||
|  | @ -747,7 +744,8 @@ func buildAndRun(pkgName string, config *compileopts.Config, stdout io.Writer, c | |||
| 		env = environmentVars | ||||
| 	} | ||||
| 
 | ||||
| 	return builder.Build(pkgName, "", config, func(result builder.BuildResult) error { | ||||
| 	format, fileExt := config.EmulatorFormat() | ||||
| 	return builder.Build(pkgName, fileExt, config, func(result builder.BuildResult) error { | ||||
| 		// If needed, set a timeout on the command. This is done in tests so | ||||
| 		// they don't waste resources on a stalled test. | ||||
| 		var ctx context.Context | ||||
|  | @ -759,12 +757,15 @@ func buildAndRun(pkgName string, config *compileopts.Config, stdout io.Writer, c | |||
| 
 | ||||
| 		// Set up the command. | ||||
| 		var name string | ||||
| 		if len(emulator) == 0 { | ||||
| 		if config.Target.Emulator == "" { | ||||
| 			name = result.Binary | ||||
| 		} else { | ||||
| 			emulator, err := config.Emulator(format, result.Binary) | ||||
| 			if err != nil { | ||||
| 				return err | ||||
| 			} | ||||
| 			name = emulator[0] | ||||
| 			emuArgs := append([]string(nil), emulator[1:]...) | ||||
| 			emuArgs = append(emuArgs, result.Binary) | ||||
| 			args = append(emuArgs, args...) | ||||
| 		} | ||||
| 		var cmd *exec.Cmd | ||||
|  | @ -779,7 +780,7 @@ func buildAndRun(pkgName string, config *compileopts.Config, stdout io.Writer, c | |||
| 		// stdout. | ||||
| 		cmd.Stdout = stdout | ||||
| 		cmd.Stderr = os.Stderr | ||||
| 		if len(emulator) != 0 && emulator[0] == "simavr" { | ||||
| 		if config.EmulatorName() == "simavr" { | ||||
| 			cmd.Stdout = nil // don't print initial load commands | ||||
| 			cmd.Stderr = stdout | ||||
| 		} | ||||
|  | @ -1572,7 +1573,7 @@ func main() { | |||
| 				os.Exit(1) | ||||
| 				return | ||||
| 			} | ||||
| 			if spec.FlashMethod == "" && spec.FlashCommand == "" && spec.Emulator == nil { | ||||
| 			if spec.FlashMethod == "" && spec.FlashCommand == "" && spec.Emulator == "" { | ||||
| 				// This doesn't look like a regular target file, but rather like | ||||
| 				// a parent target (such as targets/cortex-m.json). | ||||
| 				continue | ||||
|  |  | |||
							
								
								
									
										11
									
								
								main_test.go
									
										
									
									
									
								
							
							
						
						
									
										11
									
								
								main_test.go
									
										
									
									
									
								
							|  | @ -223,7 +223,7 @@ func runPlatTests(options compileopts.Options, tests []string, t *testing.T) { | |||
| 			runTest(name, options, t, nil, nil) | ||||
| 		}) | ||||
| 	} | ||||
| 	if len(spec.Emulator) == 0 || spec.Emulator[0] != "simavr" { | ||||
| 	if !strings.HasPrefix(spec.Emulator, "simavr ") { | ||||
| 		t.Run("env.go", func(t *testing.T) { | ||||
| 			t.Parallel() | ||||
| 			runTest("env.go", options, t, []string{"first", "second"}, []string{"ENV1=VALUE1", "ENV2=VALUE2"}) | ||||
|  | @ -257,8 +257,8 @@ func emuCheck(t *testing.T, options compileopts.Options) { | |||
| 	if err != nil { | ||||
| 		t.Fatal("failed to load target spec:", err) | ||||
| 	} | ||||
| 	if len(spec.Emulator) != 0 { | ||||
| 		_, err := exec.LookPath(spec.Emulator[0]) | ||||
| 	if spec.Emulator != "" { | ||||
| 		_, err := exec.LookPath(strings.SplitN(spec.Emulator, " ", 2)[0]) | ||||
| 		if err != nil { | ||||
| 			if errors.Is(err, exec.ErrNotFound) { | ||||
| 				t.Skipf("emulator not installed: %q", spec.Emulator[0]) | ||||
|  | @ -327,9 +327,6 @@ func runTestWithConfig(name string, t *testing.T, options compileopts.Options, c | |||
| 		t.Fatal(err) | ||||
| 	} | ||||
| 
 | ||||
| 	// make sure any special vars in the emulator definition are rewritten | ||||
| 	emulator := config.Emulator() | ||||
| 
 | ||||
| 	// Build the test binary. | ||||
| 	stdout := &bytes.Buffer{} | ||||
| 	err = buildAndRun("./"+path, config, stdout, cmdArgs, environmentVars, time.Minute, func(cmd *exec.Cmd, result builder.BuildResult) error { | ||||
|  | @ -345,7 +342,7 @@ func runTestWithConfig(name string, t *testing.T, options compileopts.Options, c | |||
| 	actual := bytes.Replace(stdout.Bytes(), []byte{'\r', '\n'}, []byte{'\n'}, -1) | ||||
| 	expected = bytes.Replace(expected, []byte{'\r', '\n'}, []byte{'\n'}, -1) // for Windows | ||||
| 
 | ||||
| 	if len(emulator) != 0 && emulator[0] == "simavr" { | ||||
| 	if config.EmulatorName() == "simavr" { | ||||
| 		// Strip simavr log formatting. | ||||
| 		actual = bytes.Replace(actual, []byte{0x1b, '[', '3', '2', 'm'}, nil, -1) | ||||
| 		actual = bytes.Replace(actual, []byte{0x1b, '[', '0', 'm'}, nil, -1) | ||||
|  |  | |||
|  | @ -6,5 +6,5 @@ | |||
| 		"-Wl,--defsym=_stack_size=512" | ||||
| 	], | ||||
| 	"flash-command": "avrdude -c arduino -p atmega328p -b 57600 -P {port} -U flash:w:{hex}:i", | ||||
| 	"emulator": ["simavr", "-m", "atmega328p", "-f", "16000000"] | ||||
| 	"emulator": "simavr -m atmega328p -f 16000000 {}" | ||||
| } | ||||
|  |  | |||
|  | @ -7,5 +7,5 @@ | |||
| 	], | ||||
| 	"flash-command": "avrdude -c arduino -p atmega328p -P {port} -U flash:w:{hex}:i", | ||||
| 	"serial-port": ["acm:2341:0043", "acm:2341:0001", "acm:2a03:0043", "acm:2341:0243"], | ||||
| 	"emulator": ["simavr", "-m", "atmega328p", "-f", "16000000"] | ||||
| 	"emulator": "simavr -m atmega328p -f 16000000 {}" | ||||
| } | ||||
|  |  | |||
|  | @ -13,5 +13,5 @@ | |||
|         "targets/avr.S", | ||||
|         "src/device/avr/atmega1284p.s" | ||||
|     ], | ||||
|     "emulator": ["simavr", "-m", "atmega1284p", "-f", "20000000"] | ||||
|     "emulator": "simavr -m atmega1284p -f 20000000 {}" | ||||
| } | ||||
|  |  | |||
|  | @ -5,5 +5,5 @@ | |||
| 	"extra-files": [ | ||||
| 		"targets/cortex-m-qemu.s" | ||||
| 	], | ||||
| 	"emulator": ["qemu-system-arm", "-machine", "lm3s6965evb", "-semihosting", "-nographic", "-kernel"] | ||||
| 	"emulator": "qemu-system-arm -machine lm3s6965evb -semihosting -nographic -kernel {}" | ||||
| } | ||||
|  |  | |||
|  | @ -6,5 +6,5 @@ | |||
| 		"-Wl,--defsym=_stack_size=128" | ||||
| 	], | ||||
| 	"flash-command": "micronucleus --run {hex}", | ||||
| 	"emulator": ["simavr", "-m", "attiny85", "-f", "16000000"] | ||||
| 	"emulator": "simavr -m attiny85 -f 16000000 {}" | ||||
| } | ||||
|  |  | |||
|  | @ -24,5 +24,5 @@ | |||
| 		"src/runtime/gc_arm.S" | ||||
| 	], | ||||
| 	"gdb": ["gdb-multiarch"], | ||||
| 	"emulator": ["mgba", "-3"] | ||||
| 	"emulator": "mgba -3 {}" | ||||
| } | ||||
|  |  | |||
|  | @ -4,5 +4,5 @@ | |||
| 	"serial": "uart", | ||||
| 	"default-stack-size": 4096, | ||||
| 	"linkerscript": "targets/hifive1-qemu.ld", | ||||
| 	"emulator": ["qemu-system-riscv32", "-machine", "sifive_e", "-nographic", "-kernel"] | ||||
| 	"emulator": "qemu-system-riscv32 -machine sifive_e -nographic -kernel {}" | ||||
| } | ||||
|  |  | |||
|  | @ -4,5 +4,5 @@ | |||
| 	"build-tags": ["virt", "qemu"], | ||||
| 	"default-stack-size": 4096, | ||||
| 	"linkerscript": "targets/riscv-qemu.ld", | ||||
| 	"emulator": ["qemu-system-riscv32", "-machine", "virt", "-nographic", "-bios", "none", "-kernel"] | ||||
| 	"emulator": "qemu-system-riscv32 -machine virt -nographic -bios none -kernel {}" | ||||
| } | ||||
|  |  | |||
|  | @ -13,6 +13,6 @@ | |||
| 		"--stack-first", | ||||
| 		"--no-demangle" | ||||
| 	], | ||||
| 	"emulator":      ["wasmtime"], | ||||
| 	"emulator":      "wasmtime {}", | ||||
| 	"wasm-abi":      "generic" | ||||
| } | ||||
|  |  | |||
|  | @ -13,6 +13,6 @@ | |||
| 		"--stack-first", | ||||
| 		"--no-demangle" | ||||
| 	], | ||||
| 	"emulator":      ["node", "{root}/targets/wasm_exec.js"], | ||||
| 	"emulator":      "node {root}/targets/wasm_exec.js {}", | ||||
| 	"wasm-abi":      "js" | ||||
| } | ||||
|  |  | |||
		Загрузка…
	
	Создание таблицы
		
		Сослаться в новой задаче
	
	 Ayke van Laethem
						Ayke van Laethem